mirror of
https://github.com/songquanpeng/message-pusher.git
synced 2025-09-26 20:21:22 +08:00
init: reinitialize from gin-template
This commit is contained in:
@@ -1,7 +0,0 @@
|
|||||||
.idea
|
|
||||||
node_modules
|
|
||||||
.env
|
|
||||||
data.db-journal
|
|
||||||
.vscode
|
|
||||||
*.db
|
|
||||||
*.*~
|
|
@@ -1,2 +0,0 @@
|
|||||||
PORT=3000
|
|
||||||
HREF="https://your.domain.com/"
|
|
30
.github/workflows/build-client.yml
vendored
30
.github/workflows/build-client.yml
vendored
@@ -1,30 +0,0 @@
|
|||||||
name: Build Client
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- '*'
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
- name: Build
|
|
||||||
run: |
|
|
||||||
cd client
|
|
||||||
go mod download
|
|
||||||
go build -ldflags "-s -w -extldflags '-static'" -o message-receiver.exe
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
client/message-receiver.exe
|
|
||||||
client/starter.vbs
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
50
.github/workflows/docker-image.yml
vendored
50
.github/workflows/docker-image.yml
vendored
@@ -1,50 +0,0 @@
|
|||||||
name: Publish Docker image
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- '*'
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
name:
|
|
||||||
description: 'reason'
|
|
||||||
required: false
|
|
||||||
jobs:
|
|
||||||
push_to_registries:
|
|
||||||
name: Push Docker image to multiple registries
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- name: Check out the repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v4
|
|
||||||
with:
|
|
||||||
images: |
|
|
||||||
justsong/message-pusher
|
|
||||||
ghcr.io/${{ github.repository }}
|
|
||||||
|
|
||||||
- name: Build and push Docker images
|
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
29
.github/workflows/github-pages.yml
vendored
Normal file
29
.github/workflows/github-pages.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: Build GitHub Pages
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
name:
|
||||||
|
description: 'Reason'
|
||||||
|
required: false
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout 🛎️
|
||||||
|
uses: actions/checkout@v2 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly.
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built.
|
||||||
|
env:
|
||||||
|
CI: ""
|
||||||
|
run: |
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Deploy 🚀
|
||||||
|
uses: JamesIves/github-pages-deploy-action@releases/v3
|
||||||
|
with:
|
||||||
|
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||||
|
BRANCH: gh-pages # The branch the action should deploy to.
|
||||||
|
FOLDER: web/build # The folder the action should deploy.
|
40
.github/workflows/linux-release.yml
vendored
Normal file
40
.github/workflows/linux-release.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Linux Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- name: Build Frontend
|
||||||
|
env:
|
||||||
|
CI: ""
|
||||||
|
run: |
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
REACT_APP_VERSION=$(git describe --tags) npm run build
|
||||||
|
cd ..
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '>=1.18.0'
|
||||||
|
- name: Build Backend
|
||||||
|
run: |
|
||||||
|
go mod download
|
||||||
|
go build -ldflags "-s -w -X 'message-pusher/common.Version=$(git describe --tags)'" -o message-pusher
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
with:
|
||||||
|
files: message-pusher
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
40
.github/workflows/macos-release.yml
vendored
Normal file
40
.github/workflows/macos-release.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: macOS Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- name: Build Frontend
|
||||||
|
env:
|
||||||
|
CI: ""
|
||||||
|
run: |
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
REACT_APP_VERSION=$(git describe --tags) npm run build
|
||||||
|
cd ..
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '>=1.18.0'
|
||||||
|
- name: Build Backend
|
||||||
|
run: |
|
||||||
|
go mod download
|
||||||
|
go build -ldflags "-X 'message-pusher/common.Version=$(git describe --tags)'" -o message-pusher-macos
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
with:
|
||||||
|
files: message-pusher-macos
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
43
.github/workflows/windows-release.yml
vendored
Normal file
43
.github/workflows/windows-release.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
name: Windows Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: windows-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- name: Build Frontend
|
||||||
|
env:
|
||||||
|
CI: ""
|
||||||
|
run: |
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
REACT_APP_VERSION=$(git describe --tags) npm run build
|
||||||
|
cd ..
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '>=1.18.0'
|
||||||
|
- name: Build Backend
|
||||||
|
run: |
|
||||||
|
go mod download
|
||||||
|
go build -ldflags "-s -w -X 'message-pusher/common.Version=$(git describe --tags)'" -o message-pusher.exe
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
with:
|
||||||
|
files: message-pusher.exe
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,9 +1,6 @@
|
|||||||
.idea
|
.idea
|
||||||
node_modules
|
|
||||||
.env
|
|
||||||
data.db-journal
|
|
||||||
.vscode
|
.vscode
|
||||||
|
upload
|
||||||
|
*.exe
|
||||||
*.db
|
*.db
|
||||||
package-lock.json
|
build
|
||||||
yarn.lock
|
|
||||||
*.*~
|
|
24
Dockerfile
24
Dockerfile
@@ -1,13 +1,25 @@
|
|||||||
FROM node:16 as builder
|
FROM node:16 as builder
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY . .
|
COPY ./web .
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM golang AS builder2
|
||||||
|
ENV GO111MODULE=on \
|
||||||
|
CGO_ENABLED=1 \
|
||||||
|
GOOS=linux \
|
||||||
|
GOARCH=amd64
|
||||||
|
|
||||||
FROM node:16-alpine
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY --from=builder /build /build
|
COPY . .
|
||||||
RUN npm install sqlite3@5.0.2 # https://github.com/TryGhost/node-sqlite3/issues/1581
|
COPY --from=builder /build/build ./web/build
|
||||||
RUN npm install pm2 -g
|
RUN go mod download
|
||||||
|
RUN go build -ldflags "-s -w" -o message-pusher
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
|
ENV PORT=3000
|
||||||
|
COPY --from=builder2 /build/message-pusher /
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["pm2-runtime", "app.js"]
|
ENTRYPOINT ["/message-pusher"]
|
||||||
|
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2020 JustSong
|
Copyright (c) 2022 JustSong
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
143
README.md
143
README.md
@@ -1,144 +1,3 @@
|
|||||||
# 消息推送服务
|
# 消息推送服务
|
||||||
## 描述
|
|
||||||
1. 多种消息推送方式:
|
|
||||||
+ 使用微信公众号测试号推送,
|
|
||||||
+ 使用微信企业号推送,
|
|
||||||
+ 使用邮箱进行推送,
|
|
||||||
+ 使用专门的桌面客户端进行推送,消息直达你的电脑,需要额外安装一个非常小的客户端,[详见此处](./client/README.md)。
|
|
||||||
2. 支持 Markdown。
|
|
||||||
3. 支持部署在 Heroku 上,无需自己的服务器,[详见此处](#在-Heroku-上的搭建步骤)。
|
|
||||||
4. Docker 一键部署:`docker run -d -p 3000:3000 justsong/message-pusher`
|
|
||||||
|
|
||||||
## 用途举例
|
README 待重写。
|
||||||
1. [整合进自己的博客系统,每当有人登录时发微信消息提醒](https://github.com/songquanpeng/blog/blob/486d63e96ef7906a6c767653a20ec2d3278e9a4a/routes/user.js#L27)。
|
|
||||||
2. 在进行深度学习模型训练时,在每个 epoch 结束后[将关键数据发送到微信](https://github.com/songquanpeng/pytorch-template/blob/b2ba113659056080d3009b3014a67e977e2851bf/solver/solver.py#L223)以方便及时监控。
|
|
||||||
3. 在各种脚本运行结束后发消息提醒,例如[监控 Github Star 数量的脚本](https://github.com/songquanpeng/scripts/blob/main/star_watcher.py),又例如[自动健康填报的脚本](https://github.com/songquanpeng/daily-report),用来通知运行结果。
|
|
||||||
|
|
||||||
## 在自己的服务器上的部署步骤
|
|
||||||
### 域名设置
|
|
||||||
先去你的云服务提供商那里添加一个子域名,解析到你的目标服务器。
|
|
||||||
|
|
||||||
### 服务器端配置
|
|
||||||
#### 方式一:手动配置环境
|
|
||||||
1. 配置 Node.js 环境,推荐使用 [nvm](https://github.com/nvm-sh/nvm)。
|
|
||||||
2. 下载代码:`git clone https://github.com/songquanpeng/message-pusher.git`,或者 `git clone https://gitee.com/songquanpeng/message-pusher`。
|
|
||||||
3. 修改根目录下的 config.js 文件:
|
|
||||||
+ (可选)可以修改监听的端口
|
|
||||||
+ (可选)配置是否选择开放注册
|
|
||||||
+ (必选)修改 href 字段,如 `https://pusher.yourdomain.com/`,注意后面要加 /,如果不修改此项,推送消息的详情页面将无法打开。
|
|
||||||
4. 安装依赖:`npm i`。
|
|
||||||
5. 安装 pm2:`npm i -g pm2`。
|
|
||||||
6. 使用 pm2 启动服务:`pm2 start ./app.js --name message-pusher`。
|
|
||||||
7. 使用 Nginx 反代我们的 Node.js 服务,默认端口 3000(你可以在 config.js 中进行修改)。
|
|
||||||
1. 修改应用根目录下的 `nginx.conf` 中的域名以及端口号,并创建软链接:`sudo ln -s /path/to/nginx.conf /etc/nginx/sites-enabled/message-pusher.conf` ,**注意修改这里的 /path/to/nginx.conf,且必须是绝对路径**,当然如果不想创建软链接的话也可以直接将配置文件拷贝过去:`sudo mv ./nginx.conf /etc/nginx/sites-enabled/message-pusher.conf`。
|
|
||||||
2. 之后使用 [certbot](https://certbot.eff.org/lets-encrypt/ubuntuxenial-nginx) 申请证书:`sudo certbot --nginx`。
|
|
||||||
3. 重启 Nginx 服务:`sudo service nginx restart`。
|
|
||||||
8. 默认用户名密码为:`admin` 和 `123456`,且默认禁止新用户注册,如需修改,请编辑 `config.js`。
|
|
||||||
|
|
||||||
#### 方式二:使用 Docker 进行部署
|
|
||||||
1. 执行命令:`docker run -d -p 3000:3000 justsong/message-pusher`
|
|
||||||
2. 接上面的第 7 步。
|
|
||||||
|
|
||||||
### 微信测试号配置
|
|
||||||
1. 首先前往[此页面](https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index)拿到 APP_ID 以及 APP_SECRET。
|
|
||||||
2. 使用微信扫描下方的测试号二维码,拿到你的 OPEN_ID。
|
|
||||||
3. 新增模板消息模板,模板标题随意,模板内容填 `{{text.DATA}}`,提交后可以拿到 TEMPLATE_ID。
|
|
||||||
4. 填写接口配置信息,URL 填 `https://你的域名/前缀/verify`,TOKEN 随意,先不要点击验证。(前缀默认和用户名相同)
|
|
||||||
5. 现在访问 `https://你的域名/`,默认用户为 admin,默认密码为 123456,登录后根据系统提示完成配置,之后点击提交按钮。
|
|
||||||
6. 之后回到微信公众平台测试号的配置页面,点击验证。
|
|
||||||
|
|
||||||
### 微信企业号配置
|
|
||||||
1. 在该[页面](https://work.weixin.qq.com/)注册微信企业号(不需要企业资质)。
|
|
||||||
2. 在该[页面](https://work.weixin.qq.com/wework_admin/frame#profile)的最下方找到企业 ID。
|
|
||||||
3. 在该[页面](https://work.weixin.qq.com/wework_admin/frame#profile/wxPlugin)找到二维码,微信扫码关注。
|
|
||||||
4. 在该[页面](https://work.weixin.qq.com/wework_admin/frame#apps)创建一个应用,之后找到应用的 AgentId 和 Secret。
|
|
||||||
5. 在该[页面](https://work.weixin.qq.com/wework_admin/frame#contacts)找到你的个人账号(一般为你的姓名拼写)。
|
|
||||||
|
|
||||||
### 验证是否配置成功
|
|
||||||
访问 `https://你的域名/前缀/Hi`,如果你的微信能够收到一条内容为 Hi 的模板消息,则配置成功。
|
|
||||||
|
|
||||||
如果出现问题,请务必仔细检查所填信息是否正确。
|
|
||||||
|
|
||||||
如果出现 `无效的 access token` 的报错,说明你设置了 ACCESS_TOKEN 但是忘记在调用时传递该值或者传递的值是错的。
|
|
||||||
|
|
||||||
|
|
||||||
## 在 Heroku 上的搭建步骤
|
|
||||||
在此之前,请先读一下“在自己的服务器上的部署步骤”这一节。
|
|
||||||
由于 Heroku 的限制,当 30 分钟内没有请求的话就会被冻结,之后再次启动时数据就丢了,因此这里我们采用配置环境变量的方式进行配置,这样即使应用冻结后再次启动配置信息依然存在。
|
|
||||||
|
|
||||||
### 一键部署
|
|
||||||
[](https://heroku.com/deploy?template=https://github.com/songquanpeng/message-pusher)
|
|
||||||
|
|
||||||
### 手动部署
|
|
||||||
1. Fork 本项目。
|
|
||||||
2. 在[此处](https://dashboard.heroku.com/new-app)新建一个 Heroku APP,名字随意,之后可以设置自己的域名。
|
|
||||||
3. 在 Deployment method 处,选择 Connect to Github,输入 message-pusher 搜索本项目,之后点击 Connect,之后启用自动部署(Enable Automatic Deploys)。
|
|
||||||
4. 点击上方的 Setting 标签,找到下面的 Config Vars 配置环境变量,有以下环境变量需要配置。
|
|
||||||
|
|
||||||
|KEY|VALUE|
|
|
||||||
|:--|:--|
|
|
||||||
|MODE|1(1 代表 Heroku 模式,该模式下应用从环境变量中读取必要信息)|
|
|
||||||
|PREFIX|你的前缀,如 admin(前缀用于区分用户,出现在请求的 api 路径中)|
|
|
||||||
|DEFAULT_METHOD|默认推送方式(test 代表微信测试号,corp 代表微信企业号,email 代表邮件推送,client 代表客户端推送)|
|
|
||||||
|HREF|服务的 href,如 https://wechat-message.herokuapp.com/ ,注意后面要有 /|
|
|
||||||
|ACCESS_TOKEN|用于验证调用者身份,防止别人使用借口发送垃圾信息,置空则不进行检查,设置该值后则需要在调用时加上 token 字段|
|
|
||||||
|WECHAT_APP_ID|你的测试号的 APP ID|
|
|
||||||
|WECHAT_APP_SECRET|你的测试号的 APP Secret|
|
|
||||||
|WECHAT_TEMPLATE_ID|你的测试号的模板消息的 ID|
|
|
||||||
|WECHAT_OPEN_ID|你的 Open ID|
|
|
||||||
|WECHAT_VERIFY_TOKEN|你自己设置的验证 token|
|
|
||||||
|EMAIL|你的默认目标邮箱|
|
|
||||||
|SMTP_SERVER|smtp 服务器地址,如 smtp.qq.com|
|
|
||||||
|SMTP_USER|smtp 服务器用户邮箱|
|
|
||||||
|SMTP_PASS|smtp 服务器用户凭据|
|
|
||||||
|CORP_ID|微信企业号 ID|
|
|
||||||
|CORP_AGENT_ID|微信企业号应用 ID|
|
|
||||||
|CORP_APP_SECRET|微信企业号应用 Secret|
|
|
||||||
|CORP_USER_ID|微信企业号用户 ID|
|
|
||||||
|
|
||||||
## 发送消息的方式
|
|
||||||
1. 发送纯文本消息:直接 HTTP GET 请求 `https://你的域名/你的前缀/消息`,缺点是有字数限制,且只能是纯文本,这是微信消息的限制。
|
|
||||||
2. 发送 Markdown 消息,调用方式分为两种:
|
|
||||||
+ GET 请求方式:`https://你的域名/前缀/?&title=消息标题&description=简短的消息描述&content=markdown格式的消息内容&email=test@qq.com&token=private`
|
|
||||||
+ POST 请求方式:请求路径为 `https://你的域名/前缀/`,参数有:
|
|
||||||
1. `type`:(可选)发送方式
|
|
||||||
+ `test`:通过微信公众号测试号推送
|
|
||||||
+ `email`:通过发送邮件的方式进行推送
|
|
||||||
+ `corp`:通过微信企业号的应用号推送
|
|
||||||
+ `client`:通过桌面客户端推送
|
|
||||||
2. `title`:(可选)消息的标题
|
|
||||||
3. `description`:(必填)消息的描述
|
|
||||||
4. `content`:(可选)消息内容,支持 Markdown
|
|
||||||
5. `email`:(可选)当该项不为空时,将强制覆盖 type 参数,强制消息类型为邮件消息,收件邮箱即此处指定的邮箱。如果 type 为 `email` 且 email 参数为空,则邮件将发送至用户设置的默认邮箱。
|
|
||||||
6. `token`:(可选)如果你设置了 ACCESS_TOKEN,则你需要附上该参数以验证身份。
|
|
||||||
|
|
||||||
## 示例程序
|
|
||||||
```python
|
|
||||||
import requests
|
|
||||||
|
|
||||||
# GET 方式
|
|
||||||
res = requests.get("https://push.iamazing.cn/admin/?title={}&description={}&token={}".format("标题", "描述", "666"))
|
|
||||||
|
|
||||||
# POST 方式
|
|
||||||
res = requests.post("https://your.domain.com/prefix/", data={
|
|
||||||
"title": "标题",
|
|
||||||
"description" : "描述",
|
|
||||||
"content": "**Markdown 内容**",
|
|
||||||
"token": "6666"
|
|
||||||
})
|
|
||||||
|
|
||||||
print(res.text)
|
|
||||||
# 输出为:{"success":true,"message":"ok"}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 待做清单
|
|
||||||
- [x] 支持多用户。
|
|
||||||
- [ ] 完善的用户管理。
|
|
||||||
- [x] 支持 Markdown。
|
|
||||||
- [x] 支持推送消息到邮箱。
|
|
||||||
- [x] 支持在 Heroku 上部署。
|
|
||||||
- [x] 更加便于部署的 [Go 语言版本](https://github.com/LeeJiangWei/go-message)。
|
|
||||||
- [x] 适配企业微信应用。
|
|
||||||
- [x] 客户端推送。
|
|
||||||
|
|
||||||
敬请期待。
|
|
95
app.js
95
app.js
@@ -1,95 +0,0 @@
|
|||||||
require('dotenv').config();
|
|
||||||
const express = require('express');
|
|
||||||
const path = require('path');
|
|
||||||
const session = require('express-session');
|
|
||||||
const flash = require('connect-flash');
|
|
||||||
const rateLimit = require('express-rate-limit');
|
|
||||||
const compression = require('compression');
|
|
||||||
const cookieParser = require('cookie-parser');
|
|
||||||
const logger = require('morgan');
|
|
||||||
const http = require('http');
|
|
||||||
const serveStatic = require('serve-static');
|
|
||||||
const config = require('./config');
|
|
||||||
const indexRouter = require('./routers/index');
|
|
||||||
const messageRouter = require('./routers/message');
|
|
||||||
const userRouter = require('./routers/user');
|
|
||||||
const { refreshToken } = require('./common/wechat');
|
|
||||||
const { initializeTokenStore, registerWebSocket } = require('./common/token');
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
const WebSocket = require('ws');
|
|
||||||
|
|
||||||
app.locals.isLogged = false;
|
|
||||||
app.locals.isAdmin = false;
|
|
||||||
app.locals.message = '';
|
|
||||||
app.locals.isErrorMessage = false;
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
// TODO: Here we need an improvement! I have tried EventEmitter but it's not working. :(
|
|
||||||
await initializeTokenStore();
|
|
||||||
await refreshToken();
|
|
||||||
setInterval(async () => refreshToken(), 100 * 60 * 1000);
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
app.set('views', path.join(__dirname, 'views'));
|
|
||||||
app.set('view engine', 'ejs');
|
|
||||||
app.set('trust proxy', true);
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
rateLimit({
|
|
||||||
windowMs: 30 * 1000,
|
|
||||||
max: 30
|
|
||||||
})
|
|
||||||
);
|
|
||||||
app.use(
|
|
||||||
'/login',
|
|
||||||
rateLimit({
|
|
||||||
windowMs: 60 * 1000,
|
|
||||||
max: 5
|
|
||||||
})
|
|
||||||
);
|
|
||||||
app.use(compression());
|
|
||||||
app.use(logger('dev'));
|
|
||||||
app.use(express.json());
|
|
||||||
app.use(express.urlencoded({ extended: false }));
|
|
||||||
app.use(cookieParser(config.cookie_secret));
|
|
||||||
app.use(
|
|
||||||
session({
|
|
||||||
resave: true,
|
|
||||||
saveUninitialized: true,
|
|
||||||
secret: config.session_secret
|
|
||||||
})
|
|
||||||
);
|
|
||||||
app.use(flash());
|
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
|
||||||
app.use(
|
|
||||||
'/public',
|
|
||||||
serveStatic(path.join(__dirname, `public`), {
|
|
||||||
maxAge: '600000'
|
|
||||||
})
|
|
||||||
);
|
|
||||||
app.use('*', (req, res, next) => {
|
|
||||||
if (req.session.user !== undefined) {
|
|
||||||
res.locals.isLogged = true;
|
|
||||||
res.locals.isAdmin = req.session.user.isAdmin;
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
app.use('/message', messageRouter);
|
|
||||||
app.use('/', indexRouter);
|
|
||||||
app.use('/', userRouter);
|
|
||||||
|
|
||||||
const server = http.createServer(app);
|
|
||||||
const wss = new WebSocket.Server({ server });
|
|
||||||
server.listen(config.port);
|
|
||||||
|
|
||||||
wss.on('connection', (ws) => {
|
|
||||||
ws.on('message', (data) => {
|
|
||||||
let message = JSON.parse(data.toString());
|
|
||||||
if (message.prefix) {
|
|
||||||
registerWebSocket(message.prefix, message.token, ws);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = app;
|
|
84
app.json
84
app.json
@@ -1,84 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "message-pusher",
|
|
||||||
"description": "消息推送服务",
|
|
||||||
"env": {
|
|
||||||
"MODE": {
|
|
||||||
"description": "Heroku 模式(请保持默认)",
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
"WECHAT_APP_ID": {
|
|
||||||
"description": "你的测试号的 APP ID",
|
|
||||||
"value": ""
|
|
||||||
},
|
|
||||||
"WECHAT_APP_SECRET": {
|
|
||||||
"description": "你的测试号的 APP Secret",
|
|
||||||
"value": ""
|
|
||||||
},
|
|
||||||
"WECHAT_TEMPLATE_ID": {
|
|
||||||
"description": "你的测试号的模板消息的 ID",
|
|
||||||
"value": ""
|
|
||||||
},
|
|
||||||
"WECHAT_OPEN_ID": {
|
|
||||||
"description": "你的 Open ID (扫描测试号二维码,列表中显示的微信号)",
|
|
||||||
"value": ""
|
|
||||||
},
|
|
||||||
"WECHAT_VERIFY_TOKEN": {
|
|
||||||
"description": "你自己设置的验证 token",
|
|
||||||
"value": ""
|
|
||||||
},
|
|
||||||
"EMAIL": {
|
|
||||||
"description": "你的默认目标邮箱",
|
|
||||||
"value": "",
|
|
||||||
"required" : false
|
|
||||||
},
|
|
||||||
"PREFIX": {
|
|
||||||
"description": "你的前缀,如 admin",
|
|
||||||
"value": "admin"
|
|
||||||
},
|
|
||||||
"SMTP_SERVER": {
|
|
||||||
"description": "smtp 服务器地址,如 smtp.qq.com",
|
|
||||||
"value": "smtp.qq.com",
|
|
||||||
"required" : false
|
|
||||||
},
|
|
||||||
"SMTP_USER": {
|
|
||||||
"description": "smtp 服务器用户邮箱",
|
|
||||||
"value": "",
|
|
||||||
"required" : false
|
|
||||||
},
|
|
||||||
"SMTP_PASS": {
|
|
||||||
"description": "smtp 服务器用户凭据",
|
|
||||||
"value": "",
|
|
||||||
"required" : false
|
|
||||||
},
|
|
||||||
"HREF": {
|
|
||||||
"description": "服务的 href,如 https://wechat-message.herokuapp.com/ ,注意后面要有 /",
|
|
||||||
"value": ""
|
|
||||||
},
|
|
||||||
"DEFAULT_METHOD" : {
|
|
||||||
"description": "默认方式方式(test 代表微信测试号,cor 代表微信企业号,email 代表邮件推送)",
|
|
||||||
"value": ""
|
|
||||||
},
|
|
||||||
"CORP_ID" : {
|
|
||||||
"description": "微信企业号 ID",
|
|
||||||
"value": ""
|
|
||||||
},
|
|
||||||
"CORP_AGENT_ID" : {
|
|
||||||
"description": "微信企业号应用 ID",
|
|
||||||
"value": ""
|
|
||||||
},
|
|
||||||
"CORP_APP_SECRET" : {
|
|
||||||
"description": "微信企业号应用 Secret",
|
|
||||||
"value": ""
|
|
||||||
},
|
|
||||||
"CORP_USER_ID" : {
|
|
||||||
"description": "微信企业号用户 ID",
|
|
||||||
"value": ""
|
|
||||||
},
|
|
||||||
"ACCESS_TOKEN" : {
|
|
||||||
"description": "用于验证调用者身份,防止别人使用借口发送垃圾信息,置空则不进行检查,设置该值后则需要在调用时加上 token 字段",
|
|
||||||
"value": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"website": "https://github.com/songquanpeng/message-pusher",
|
|
||||||
"repository": "https://github.com/songquanpeng/message-pusher"
|
|
||||||
}
|
|
2
client/.gitignore
vendored
2
client/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
.idea
|
|
||||||
*.exe
|
|
@@ -1,18 +0,0 @@
|
|||||||
# Message Pusher 桌面客户端
|
|
||||||
## 描述
|
|
||||||
该客户端用于支持 `client` 消息推送方式。
|
|
||||||
|
|
||||||
注意,为防止未授权的 WebSocket 连接,推荐设置 ACCESS_TOKEN。
|
|
||||||
|
|
||||||
启动参数:`./client.exe --url http://your.domain.com:port/prefix --token private`
|
|
||||||
|
|
||||||
也可以通过设置环境变量来传递配置:`MESSAGE_PUSHER_URL` & `MESSAGE_PUSHER_TOKEN`
|
|
||||||
|
|
||||||
## 原理
|
|
||||||
客户端启动后与 message-pusher 服务器建立一个 WebSocket 连接,通过该连接接受要推送消息,并调用系统的消息通知接口进行消息通知。
|
|
||||||
|
|
||||||
## 安装
|
|
||||||
1. 首先前往 [Release 页面](https://github.com/songquanpeng/message-pusher/releases)下载可执行文件和 vbs 脚本文件(该脚本文件目的是为了隐藏窗口)。
|
|
||||||
2. 编辑 vbs 脚本文件,将其中的可执行文件的路径和后面的参数改成符合你的情况的值。
|
|
||||||
3. 之后对 vbs 脚本文件右键创建快捷方式。
|
|
||||||
5. 之后把该快捷方式放到开机启动文件夹内:`C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp`
|
|
@@ -1,11 +0,0 @@
|
|||||||
module client
|
|
||||||
|
|
||||||
go 1.16
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1 // indirect
|
|
||||||
github.com/godbus/dbus/v5 v5.0.4 // indirect
|
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20210603182125-eeedf4a0e899 // indirect
|
|
||||||
github.com/gorilla/websocket v1.4.2 // indirect
|
|
||||||
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273 // indirect
|
|
||||||
)
|
|
330
client/go.sum
330
client/go.sum
@@ -1,330 +0,0 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
|
||||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
|
||||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
|
||||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
|
||||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
|
||||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
|
||||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
|
||||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
|
||||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
|
||||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
|
||||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
|
||||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
|
||||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
|
||||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
|
||||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
|
||||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
|
||||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
|
||||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
|
||||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
|
||||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
|
||||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
|
||||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
|
||||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
|
||||||
github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1 h1:Xh9mvwEmhbdXlRSsgn+N0zj/NqnKvpeqL08oKDHln2s=
|
|
||||||
github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1/go.mod h1:ElSskYZe3oM8kThaHGJ+kiN2yyUMVXMZ7WxF9QqLDS8=
|
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
|
||||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
|
|
||||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
|
|
||||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
|
||||||
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
|
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
|
||||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
|
||||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
|
||||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
|
||||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20210603182125-eeedf4a0e899 h1:3RAjDwfQm52SdfOxlHfcstP1mNPKf7IvIWn/I4ENlFQ=
|
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20210603182125-eeedf4a0e899/go.mod h1:MtKwTfDNYAP5EtbQSMYjTSqvj1aXJKQRASWq3bwaP+g=
|
|
||||||
github.com/gopherjs/gopherwasm v1.1.0 h1:fA2uLoctU5+T3OhOn2vYP0DVT6pxc7xhTlBB1paATqQ=
|
|
||||||
github.com/gopherjs/gopherwasm v1.1.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI=
|
|
||||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
|
||||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
|
||||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
|
||||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
|
||||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
|
||||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
|
||||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
|
||||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
|
||||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
|
||||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
|
||||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
|
||||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
|
||||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
|
||||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
|
||||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
|
||||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
|
||||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
|
||||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
|
||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
|
||||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
|
||||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
|
||||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
|
||||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
|
||||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
|
||||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
|
||||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
|
||||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
|
||||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
|
||||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
|
||||||
github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
|
||||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
|
|
||||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
|
|
||||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
|
||||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
|
||||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
|
||||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
|
||||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
|
||||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
|
||||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
|
||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
||||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
|
||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
|
||||||
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
|
||||||
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
|
||||||
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
|
||||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
|
||||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
|
||||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
|
||||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
|
||||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
|
||||||
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
|
|
||||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
|
||||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
|
||||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
|
||||||
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
|
|
||||||
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
|
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
|
||||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
|
||||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
|
||||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
|
||||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
|
||||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
|
||||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
|
||||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
|
||||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
|
||||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
|
||||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
|
||||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273 h1:faDu4veV+8pcThn4fewv6TVlNCezafGoC1gM/mxQLbQ=
|
|
||||||
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
|
||||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
|
||||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
|
||||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
|
||||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
|
||||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
|
||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
|
||||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
|
||||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
|
||||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
|
@@ -1,92 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"log"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
conn = flag.String("url", "", "connection url")
|
|
||||||
token = flag.String("token", "", "the access token")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Verification struct {
|
|
||||||
Prefix string `json:"prefix"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Ping struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
connString := *conn
|
|
||||||
if connString == "" {
|
|
||||||
connString = os.Getenv("MESSAGE_PUSHER_URL")
|
|
||||||
}
|
|
||||||
if *token == "" {
|
|
||||||
*token = os.Getenv("MESSAGE_PUSHER_TOKEN")
|
|
||||||
}
|
|
||||||
connUrl, err := url.Parse(connString)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Failed to parse connection url", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
scheme := "ws"
|
|
||||||
if connUrl.Scheme == "https" {
|
|
||||||
scheme = "wss"
|
|
||||||
}
|
|
||||||
u := url.URL{Scheme: scheme, Host: connUrl.Host, Path: "/"}
|
|
||||||
verification := &Verification{
|
|
||||||
Prefix: connUrl.Path[1:],
|
|
||||||
Token: *token,
|
|
||||||
}
|
|
||||||
ping := &Ping{}
|
|
||||||
ticker := time.NewTicker(60 * time.Second)
|
|
||||||
for {
|
|
||||||
log.Printf("Connecting to %s...\n", u.String())
|
|
||||||
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Failed to connect to server:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Printf("Server connected.\n")
|
|
||||||
|
|
||||||
err = c.WriteJSON(verification)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
if err := c.WriteJSON(ping); err != nil {
|
|
||||||
log.Println("Error occurred when send ping message:", err)
|
|
||||||
log.Println("Connection lost, retrying...")
|
|
||||||
_ = c.Close()
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
log.Println("Ping message sent.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
for {
|
|
||||||
var message = new(Message)
|
|
||||||
err = c.ReadJSON(message)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Error occurred when read message:", err)
|
|
||||||
log.Println("Connection lost, retrying...")
|
|
||||||
_ = c.Close()
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
log.Println("New message arrived.")
|
|
||||||
Notify(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,18 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/gen2brain/beeep"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Message struct {
|
|
||||||
Title string
|
|
||||||
Description string
|
|
||||||
}
|
|
||||||
|
|
||||||
func Notify(message *Message) {
|
|
||||||
err := beeep.Notify(message.Title, message.Description, "assets/information.png")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,3 +0,0 @@
|
|||||||
Set WshShell = CreateObject("WScript.Shell")
|
|
||||||
WshShell.Run chr(34) & "C:\Users\song\Projects\message-pusher\client\message-receiver.exe ""--url https://server_host/prefix" & Chr(34), 0
|
|
||||||
Set WshShell = Nothing
|
|
@@ -1,25 +0,0 @@
|
|||||||
const WebSocket = require('ws');
|
|
||||||
const { tokenStore } = require('./token');
|
|
||||||
|
|
||||||
async function pushClientMessage(userPrefix, message) {
|
|
||||||
let user = tokenStore.get(userPrefix);
|
|
||||||
if (!user || !user.ws || user.ws.readyState !== WebSocket.OPEN) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `客户端未连接`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let data = {
|
|
||||||
title: message.title,
|
|
||||||
description: message.description,
|
|
||||||
};
|
|
||||||
user.ws.send(JSON.stringify(data));
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: '消息已发送至客户端',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
pushClientMessage,
|
|
||||||
};
|
|
80
common/constants.go
Normal file
80
common/constants.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var StartTime = time.Now().Unix() // unit: second
|
||||||
|
var Version = "v0.0.0"
|
||||||
|
var SystemName = "消息推送服务"
|
||||||
|
var ServerAddress = "http://localhost:3000"
|
||||||
|
var Footer = ""
|
||||||
|
|
||||||
|
// Any options with "Secret", "Token" in its key won't be return by GetOptions
|
||||||
|
|
||||||
|
var SessionSecret = uuid.New().String()
|
||||||
|
var SQLitePath = ".message-pusher.db"
|
||||||
|
|
||||||
|
var OptionMap map[string]string
|
||||||
|
var OptionMapRWMutex sync.RWMutex
|
||||||
|
|
||||||
|
var ItemsPerPage = 10
|
||||||
|
|
||||||
|
var PasswordLoginEnabled = true
|
||||||
|
var PasswordRegisterEnabled = true
|
||||||
|
var EmailVerificationEnabled = false
|
||||||
|
var GitHubOAuthEnabled = false
|
||||||
|
var WeChatAuthEnabled = false
|
||||||
|
|
||||||
|
var SMTPServer = ""
|
||||||
|
var SMTPAccount = ""
|
||||||
|
var SMTPToken = ""
|
||||||
|
|
||||||
|
var GitHubClientId = ""
|
||||||
|
var GitHubClientSecret = ""
|
||||||
|
|
||||||
|
var WeChatServerAddress = ""
|
||||||
|
var WeChatServerToken = ""
|
||||||
|
var WeChatAccountQRCodeImageURL = ""
|
||||||
|
|
||||||
|
const (
|
||||||
|
RoleGuestUser = 0
|
||||||
|
RoleCommonUser = 1
|
||||||
|
RoleAdminUser = 10
|
||||||
|
RoleRootUser = 100
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
FileUploadPermission = RoleGuestUser
|
||||||
|
FileDownloadPermission = RoleGuestUser
|
||||||
|
ImageUploadPermission = RoleGuestUser
|
||||||
|
ImageDownloadPermission = RoleGuestUser
|
||||||
|
)
|
||||||
|
|
||||||
|
// All duration's unit is seconds
|
||||||
|
// Shouldn't larger then RateLimitKeyExpirationDuration
|
||||||
|
var (
|
||||||
|
GlobalApiRateLimitNum = 60
|
||||||
|
GlobalApiRateLimitDuration int64 = 3 * 60
|
||||||
|
|
||||||
|
GlobalWebRateLimitNum = 60
|
||||||
|
GlobalWebRateLimitDuration int64 = 3 * 60
|
||||||
|
|
||||||
|
UploadRateLimitNum = 10
|
||||||
|
UploadRateLimitDuration int64 = 60
|
||||||
|
|
||||||
|
DownloadRateLimitNum = 10
|
||||||
|
DownloadRateLimitDuration int64 = 60
|
||||||
|
|
||||||
|
CriticalRateLimitNum = 20
|
||||||
|
CriticalRateLimitDuration int64 = 20 * 60
|
||||||
|
)
|
||||||
|
|
||||||
|
var RateLimitKeyExpirationDuration = 20 * time.Minute
|
||||||
|
|
||||||
|
const (
|
||||||
|
UserStatusEnabled = 1 // don't use 0, 0 is the default value!
|
||||||
|
UserStatusDisabled = 2 // also don't use 0
|
||||||
|
)
|
14
common/crypto.go
Normal file
14
common/crypto.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import "golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
func Password2Hash(password string) (string, error) {
|
||||||
|
passwordBytes := []byte(password)
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost)
|
||||||
|
return string(hashedPassword), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidatePasswordAndHash(password string, hash string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
|
return err == nil
|
||||||
|
}
|
@@ -1,9 +0,0 @@
|
|||||||
const { Sequelize } = require('sequelize');
|
|
||||||
|
|
||||||
const sequelize = new Sequelize({
|
|
||||||
dialect: 'sqlite',
|
|
||||||
storage: 'data.db',
|
|
||||||
logging: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = sequelize;
|
|
14
common/email.go
Normal file
14
common/email.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import "gopkg.in/gomail.v2"
|
||||||
|
|
||||||
|
func SendEmail(subject string, receiver string, content string) error {
|
||||||
|
m := gomail.NewMessage()
|
||||||
|
m.SetHeader("From", SMTPAccount)
|
||||||
|
m.SetHeader("To", receiver)
|
||||||
|
m.SetHeader("Subject", subject)
|
||||||
|
m.SetBody("text/html", content)
|
||||||
|
d := gomail.NewDialer(SMTPServer, 587, SMTPAccount, SMTPToken)
|
||||||
|
err := d.DialAndSend(m)
|
||||||
|
return err
|
||||||
|
}
|
@@ -1,51 +0,0 @@
|
|||||||
const nodemailer = require('nodemailer');
|
|
||||||
const { tokenStore } = require('./token');
|
|
||||||
const config = require('../config');
|
|
||||||
const { md2html } = require('./utils');
|
|
||||||
|
|
||||||
async function pushEmailMessage(userPrefix, message) {
|
|
||||||
let user = tokenStore.get(userPrefix);
|
|
||||||
if (!user) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `不存在的前缀:${userPrefix},请注意大小写`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let transporter = nodemailer.createTransport({
|
|
||||||
host: user.smtpServer,
|
|
||||||
secure: true,
|
|
||||||
auth: {
|
|
||||||
user: user.smtpUser,
|
|
||||||
pass: user.smtpPass,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let targetEmail = user.email;
|
|
||||||
if (message.email) {
|
|
||||||
targetEmail = message.email;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await transporter.sendMail({
|
|
||||||
from: `"消息推送服务" <${user.smtpUser}>`,
|
|
||||||
to: targetEmail,
|
|
||||||
subject: message.description,
|
|
||||||
text: message.content,
|
|
||||||
html: md2html(message.content),
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: 'ok',
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: e.message,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
pushEmailMessage,
|
|
||||||
};
|
|
32
common/embed-file-system.go
Normal file
32
common/embed-file-system.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"github.com/gin-gonic/contrib/static"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Credit: https://github.com/gin-contrib/static/issues/19
|
||||||
|
|
||||||
|
type embedFileSystem struct {
|
||||||
|
http.FileSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e embedFileSystem) Exists(prefix string, path string) bool {
|
||||||
|
_, err := e.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem {
|
||||||
|
efs, err := fs.Sub(fsEmbed, targetPath)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return embedFileSystem{
|
||||||
|
FileSystem: http.FS(efs),
|
||||||
|
}
|
||||||
|
}
|
78
common/init.go
Normal file
78
common/init.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Port = flag.Int("port", 3000, "the listening port")
|
||||||
|
PrintVersion = flag.Bool("version", false, "print version and exit")
|
||||||
|
LogDir = flag.String("log-dir", "", "specify the log directory")
|
||||||
|
//Host = flag.String("host", "localhost", "the server's ip address or domain")
|
||||||
|
//Path = flag.String("path", "", "specify a local path to public")
|
||||||
|
//VideoPath = flag.String("video", "", "specify a video folder to public")
|
||||||
|
//NoBrowser = flag.Bool("no-browser", false, "open browser or not")
|
||||||
|
)
|
||||||
|
|
||||||
|
// UploadPath Maybe override by ENV_VAR
|
||||||
|
var UploadPath = "upload"
|
||||||
|
|
||||||
|
//var ExplorerRootPath = UploadPath
|
||||||
|
//var ImageUploadPath = "upload/images"
|
||||||
|
//var VideoServePath = "upload"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *PrintVersion {
|
||||||
|
fmt.Println(Version)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("SESSION_SECRET") != "" {
|
||||||
|
SessionSecret = os.Getenv("SESSION_SECRET")
|
||||||
|
}
|
||||||
|
if os.Getenv("SQLITE_PATH") != "" {
|
||||||
|
SQLitePath = os.Getenv("SQLITE_PATH")
|
||||||
|
}
|
||||||
|
if os.Getenv("UPLOAD_PATH") != "" {
|
||||||
|
UploadPath = os.Getenv("UPLOAD_PATH")
|
||||||
|
//ExplorerRootPath = UploadPath
|
||||||
|
//ImageUploadPath = path.Join(UploadPath, "images")
|
||||||
|
//VideoServePath = UploadPath
|
||||||
|
}
|
||||||
|
if *LogDir != "" {
|
||||||
|
var err error
|
||||||
|
*LogDir, err = filepath.Abs(*LogDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(*LogDir); os.IsNotExist(err) {
|
||||||
|
err = os.Mkdir(*LogDir, 0777)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//if *Path != "" {
|
||||||
|
// ExplorerRootPath = *Path
|
||||||
|
//}
|
||||||
|
//if *VideoPath != "" {
|
||||||
|
// VideoServePath = *VideoPath
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//ExplorerRootPath, _ = filepath.Abs(ExplorerRootPath)
|
||||||
|
//VideoServePath, _ = filepath.Abs(VideoServePath)
|
||||||
|
//ImageUploadPath, _ = filepath.Abs(ImageUploadPath)
|
||||||
|
//
|
||||||
|
if _, err := os.Stat(UploadPath); os.IsNotExist(err) {
|
||||||
|
_ = os.Mkdir(UploadPath, 0777)
|
||||||
|
}
|
||||||
|
//if _, err := os.Stat(ImageUploadPath); os.IsNotExist(err) {
|
||||||
|
// _ = os.Mkdir(ImageUploadPath, 0777)
|
||||||
|
//}
|
||||||
|
}
|
39
common/logger.go
Normal file
39
common/logger.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupGinLog() {
|
||||||
|
if *LogDir != "" {
|
||||||
|
commonLogPath := filepath.Join(*LogDir, "common.log")
|
||||||
|
errorLogPath := filepath.Join(*LogDir, "error.log")
|
||||||
|
commonFd, err := os.OpenFile(commonLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("failed to open log file")
|
||||||
|
}
|
||||||
|
errorFd, err := os.OpenFile(errorLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("failed to open log file")
|
||||||
|
}
|
||||||
|
gin.DefaultWriter = io.MultiWriter(os.Stdout, commonFd)
|
||||||
|
gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, errorFd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SysLog(s string) {
|
||||||
|
t := time.Now()
|
||||||
|
_, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FatalLog(v ...any) {
|
||||||
|
t := time.Now()
|
||||||
|
_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
@@ -1,62 +0,0 @@
|
|||||||
const {
|
|
||||||
getUserDefaultMethod,
|
|
||||||
checkAccessToken,
|
|
||||||
checkPrefix,
|
|
||||||
} = require('./token');
|
|
||||||
const { pushWeChatMessage } = require('./wechat');
|
|
||||||
const { pushWeChatCorpMessage } = require('./wechat-corp');
|
|
||||||
const { pushEmailMessage } = require('./email');
|
|
||||||
const { pushClientMessage } = require('./client');
|
|
||||||
const { Message } = require('../models');
|
|
||||||
|
|
||||||
async function processMessage(userPrefix, message) {
|
|
||||||
if (!checkPrefix(userPrefix)) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `不存在的用户前缀:${userPrefix}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!checkAccessToken(userPrefix, message.token)) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `无效的访问凭证,请检查 token 参数是否正确`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (message.email) {
|
|
||||||
// If message has the attribute "email", override its type.
|
|
||||||
message.type = 'email';
|
|
||||||
}
|
|
||||||
if (!message.type) {
|
|
||||||
message.type = getUserDefaultMethod(userPrefix);
|
|
||||||
}
|
|
||||||
if (message.content && message.type !== 'email') {
|
|
||||||
// If message is not email type, we should save it because we have to serve the page.
|
|
||||||
message = await Message.create(message, { raw: true });
|
|
||||||
}
|
|
||||||
let result;
|
|
||||||
switch (message.type) {
|
|
||||||
case 'test': // WeChat message
|
|
||||||
result = await pushWeChatMessage(userPrefix, message);
|
|
||||||
break;
|
|
||||||
case 'email': // Email message
|
|
||||||
result = await pushEmailMessage(userPrefix, message);
|
|
||||||
break;
|
|
||||||
case 'corp': // WeChat corp message
|
|
||||||
result = await pushWeChatCorpMessage(userPrefix, message);
|
|
||||||
break;
|
|
||||||
case 'client': // Client message
|
|
||||||
result = await pushClientMessage(userPrefix, message);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
result = {
|
|
||||||
success: false,
|
|
||||||
message: `不支持的消息类型:${message.type}`,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
processMessage,
|
|
||||||
};
|
|
70
common/rate-limit.go
Normal file
70
common/rate-limit.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InMemoryRateLimiter struct {
|
||||||
|
store map[string]*[]int64
|
||||||
|
mutex sync.Mutex
|
||||||
|
expirationDuration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *InMemoryRateLimiter) Init(expirationDuration time.Duration) {
|
||||||
|
if l.store == nil {
|
||||||
|
l.mutex.Lock()
|
||||||
|
if l.store == nil {
|
||||||
|
l.store = make(map[string]*[]int64)
|
||||||
|
l.expirationDuration = expirationDuration
|
||||||
|
if expirationDuration > 0 {
|
||||||
|
go l.clearExpiredItems()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l.mutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *InMemoryRateLimiter) clearExpiredItems() {
|
||||||
|
for {
|
||||||
|
time.Sleep(l.expirationDuration)
|
||||||
|
l.mutex.Lock()
|
||||||
|
now := time.Now().Unix()
|
||||||
|
for key := range l.store {
|
||||||
|
queue := l.store[key]
|
||||||
|
size := len(*queue)
|
||||||
|
if size == 0 || now-(*queue)[size-1] > int64(l.expirationDuration.Seconds()) {
|
||||||
|
delete(l.store, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l.mutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request parameter duration's unit is seconds
|
||||||
|
func (l *InMemoryRateLimiter) Request(key string, maxRequestNum int, duration int64) bool {
|
||||||
|
l.mutex.Lock()
|
||||||
|
defer l.mutex.Unlock()
|
||||||
|
// [old <-- new]
|
||||||
|
queue, ok := l.store[key]
|
||||||
|
now := time.Now().Unix()
|
||||||
|
if ok {
|
||||||
|
if len(*queue) < maxRequestNum {
|
||||||
|
*queue = append(*queue, now)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
if now-(*queue)[0] >= duration {
|
||||||
|
*queue = (*queue)[1:]
|
||||||
|
*queue = append(*queue, now)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s := make([]int64, 0, maxRequestNum)
|
||||||
|
l.store[key] = &s
|
||||||
|
*(l.store[key]) = append(*(l.store[key]), now)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
39
common/redis.go
Normal file
39
common/redis.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var RDB *redis.Client
|
||||||
|
var RedisEnabled = true
|
||||||
|
|
||||||
|
// InitRedisClient This function is called after init()
|
||||||
|
func InitRedisClient() (err error) {
|
||||||
|
if os.Getenv("REDIS_CONN_STRING") == "" {
|
||||||
|
RedisEnabled = false
|
||||||
|
SysLog("REDIS_CONN_STRING not set, Redis is not enabled")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
RDB = redis.NewClient(opt)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err = RDB.Ping(ctx).Result()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseRedisOption() *redis.Options {
|
||||||
|
opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return opt
|
||||||
|
}
|
109
common/token.js
109
common/token.js
@@ -1,109 +0,0 @@
|
|||||||
const { User } = require('../models');
|
|
||||||
|
|
||||||
const tokenStore = new Map();
|
|
||||||
|
|
||||||
async function initializeTokenStore() {
|
|
||||||
let users = [];
|
|
||||||
if (process.env.MODE === '1') {
|
|
||||||
console.log('Current mode is Heroku mode.');
|
|
||||||
let user = {
|
|
||||||
// Common
|
|
||||||
prefix: process.env.PREFIX,
|
|
||||||
accessToken: process.env.ACCESS_TOKEN,
|
|
||||||
defaultMethod: process.env.DEFAULT_METHOD,
|
|
||||||
// WeChat public account
|
|
||||||
wechatAppId: process.env.WECHAT_APP_ID,
|
|
||||||
wechatAppSecret: process.env.WECHAT_APP_SECRET,
|
|
||||||
wechatTemplateId: process.env.WECHAT_TEMPLATE_ID,
|
|
||||||
wechatOpenId: process.env.WECHAT_OPEN_ID,
|
|
||||||
wechatVerifyToken: process.env.WECHAT_VERIFY_TOKEN,
|
|
||||||
// Email
|
|
||||||
email: process.env.EMAIL,
|
|
||||||
smtpServer: process.env.SMTP_SERVER,
|
|
||||||
smtpUser: process.env.SMTP_USER,
|
|
||||||
smtpPass: process.env.SMTP_PASS,
|
|
||||||
// WeChat corporation account
|
|
||||||
corpId: process.env.CORP_ID,
|
|
||||||
corpAgentId: process.env.CORP_AGENT_ID,
|
|
||||||
corpAppSecret: process.env.CORP_APP_SECRET,
|
|
||||||
corpUserId: process.env.CORP_USER_ID,
|
|
||||||
};
|
|
||||||
users.push(user);
|
|
||||||
} else {
|
|
||||||
users = await User.findAll({
|
|
||||||
raw: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
users.forEach((user) => {
|
|
||||||
if (user.prefix) {
|
|
||||||
tokenStore.set(user.prefix, {
|
|
||||||
// Common
|
|
||||||
accessToken: user.accessToken,
|
|
||||||
defaultMethod: user.defaultMethod,
|
|
||||||
// WeChat test account
|
|
||||||
wechatAppId: user.wechatAppId,
|
|
||||||
wechatAppSecret: user.wechatAppSecret,
|
|
||||||
wechatTemplateId: user.wechatTemplateId,
|
|
||||||
wechatOpenId: user.wechatOpenId,
|
|
||||||
wechatVerifyToken: user.wechatVerifyToken,
|
|
||||||
wechatToken: '',
|
|
||||||
// Email
|
|
||||||
email: user.email,
|
|
||||||
smtpServer: user.smtpServer,
|
|
||||||
smtpUser: user.smtpUser,
|
|
||||||
smtpPass: user.smtpPass,
|
|
||||||
// WeChat corporation account
|
|
||||||
corpId: user.corpId,
|
|
||||||
corpAgentId: user.corpAgentId,
|
|
||||||
corpAppSecret: user.corpAppSecret,
|
|
||||||
corpUserId: user.corpUserId,
|
|
||||||
corpToken: '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log('Token store initialized.');
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTokenStore(prefix, key, value) {
|
|
||||||
let user = tokenStore.get(prefix);
|
|
||||||
user[key] = value;
|
|
||||||
tokenStore.set(prefix, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUserDefaultMethod(prefix) {
|
|
||||||
let user = tokenStore.get(prefix);
|
|
||||||
return user.defaultMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkAccessToken(prefix, token) {
|
|
||||||
let user = tokenStore.get(prefix);
|
|
||||||
if (user.accessToken === '') {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return user.accessToken === token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkPrefix(prefix) {
|
|
||||||
let user = tokenStore.get(prefix);
|
|
||||||
return user !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerWebSocket(prefix, token, ws) {
|
|
||||||
let user = tokenStore.get(prefix);
|
|
||||||
if (user && user.accessToken === token) {
|
|
||||||
updateTokenStore(prefix, 'ws', ws);
|
|
||||||
} else {
|
|
||||||
ws.terminate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
initializeTokenStore,
|
|
||||||
updateTokenStore,
|
|
||||||
getUserDefaultMethod,
|
|
||||||
tokenStore,
|
|
||||||
checkAccessToken,
|
|
||||||
checkPrefix,
|
|
||||||
registerWebSocket,
|
|
||||||
};
|
|
133
common/utils.go
Normal file
133
common/utils.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func OpenBrowser(url string) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
err = exec.Command("xdg-open", url).Start()
|
||||||
|
case "windows":
|
||||||
|
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||||
|
case "darwin":
|
||||||
|
err = exec.Command("open", url).Start()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetIp() (ip string) {
|
||||||
|
ips, err := net.InterfaceAddrs()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, a := range ips {
|
||||||
|
if ipNet, ok := a.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
|
||||||
|
if ipNet.IP.To4() != nil {
|
||||||
|
ip = ipNet.IP.String()
|
||||||
|
if strings.HasPrefix(ip, "10") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(ip, "172") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(ip, "192.168") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ip = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var sizeKB = 1024
|
||||||
|
var sizeMB = sizeKB * 1024
|
||||||
|
var sizeGB = sizeMB * 1024
|
||||||
|
|
||||||
|
func Bytes2Size(num int64) string {
|
||||||
|
numStr := ""
|
||||||
|
unit := "B"
|
||||||
|
if num/int64(sizeGB) > 1 {
|
||||||
|
numStr = fmt.Sprintf("%.2f", float64(num)/float64(sizeGB))
|
||||||
|
unit = "GB"
|
||||||
|
} else if num/int64(sizeMB) > 1 {
|
||||||
|
numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeMB)))
|
||||||
|
unit = "MB"
|
||||||
|
} else if num/int64(sizeKB) > 1 {
|
||||||
|
numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeKB)))
|
||||||
|
unit = "KB"
|
||||||
|
} else {
|
||||||
|
numStr = fmt.Sprintf("%d", num)
|
||||||
|
}
|
||||||
|
return numStr + " " + unit
|
||||||
|
}
|
||||||
|
|
||||||
|
func Seconds2Time(num int) (time string) {
|
||||||
|
if num/31104000 > 0 {
|
||||||
|
time += strconv.Itoa(num/31104000) + " 年 "
|
||||||
|
num %= 31104000
|
||||||
|
}
|
||||||
|
if num/2592000 > 0 {
|
||||||
|
time += strconv.Itoa(num/2592000) + " 个月 "
|
||||||
|
num %= 2592000
|
||||||
|
}
|
||||||
|
if num/86400 > 0 {
|
||||||
|
time += strconv.Itoa(num/86400) + " 天 "
|
||||||
|
num %= 86400
|
||||||
|
}
|
||||||
|
if num/3600 > 0 {
|
||||||
|
time += strconv.Itoa(num/3600) + " 小时 "
|
||||||
|
num %= 3600
|
||||||
|
}
|
||||||
|
if num/60 > 0 {
|
||||||
|
time += strconv.Itoa(num/60) + " 分钟 "
|
||||||
|
num %= 60
|
||||||
|
}
|
||||||
|
time += strconv.Itoa(num) + " 秒"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func Interface2String(inter interface{}) string {
|
||||||
|
switch inter.(type) {
|
||||||
|
case string:
|
||||||
|
return inter.(string)
|
||||||
|
case int:
|
||||||
|
return fmt.Sprintf("%d", inter.(int))
|
||||||
|
case float64:
|
||||||
|
return fmt.Sprintf("%f", inter.(float64))
|
||||||
|
}
|
||||||
|
return "Not Implemented"
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnescapeHTML(x string) interface{} {
|
||||||
|
return template.HTML(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IntMax(a int, b int) int {
|
||||||
|
if a >= b {
|
||||||
|
return a
|
||||||
|
} else {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUUID() string {
|
||||||
|
code := uuid.New().String()
|
||||||
|
code = strings.Replace(code, "-", "", -1)
|
||||||
|
return code
|
||||||
|
}
|
@@ -1,13 +0,0 @@
|
|||||||
const lexer = require('marked').lexer;
|
|
||||||
const parser = require('marked').parser;
|
|
||||||
|
|
||||||
function md2html(markdown) {
|
|
||||||
if (markdown) {
|
|
||||||
return parser(lexer(markdown));
|
|
||||||
}
|
|
||||||
return markdown;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
md2html,
|
|
||||||
};
|
|
9
common/validate.go
Normal file
9
common/validate.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import "github.com/go-playground/validator/v10"
|
||||||
|
|
||||||
|
var Validate *validator.Validate
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Validate = validator.New()
|
||||||
|
}
|
77
common/verification.go
Normal file
77
common/verification.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type verificationValue struct {
|
||||||
|
code string
|
||||||
|
time time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
EmailVerificationPurpose = "v"
|
||||||
|
PasswordResetPurpose = "r"
|
||||||
|
)
|
||||||
|
|
||||||
|
var verificationMutex sync.Mutex
|
||||||
|
var verificationMap map[string]verificationValue
|
||||||
|
var verificationMapMaxSize = 10
|
||||||
|
var VerificationValidMinutes = 10
|
||||||
|
|
||||||
|
func GenerateVerificationCode(length int) string {
|
||||||
|
code := uuid.New().String()
|
||||||
|
code = strings.Replace(code, "-", "", -1)
|
||||||
|
if length == 0 {
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
return code[:length]
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterVerificationCodeWithKey(key string, code string, purpose string) {
|
||||||
|
verificationMutex.Lock()
|
||||||
|
defer verificationMutex.Unlock()
|
||||||
|
verificationMap[purpose+key] = verificationValue{
|
||||||
|
code: code,
|
||||||
|
time: time.Now(),
|
||||||
|
}
|
||||||
|
if len(verificationMap) > verificationMapMaxSize {
|
||||||
|
removeExpiredPairs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifyCodeWithKey(key string, code string, purpose string) bool {
|
||||||
|
verificationMutex.Lock()
|
||||||
|
defer verificationMutex.Unlock()
|
||||||
|
value, okay := verificationMap[purpose+key]
|
||||||
|
now := time.Now()
|
||||||
|
if !okay || int(now.Sub(value.time).Seconds()) >= VerificationValidMinutes*60 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return code == value.code
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteKey(key string, purpose string) {
|
||||||
|
verificationMutex.Lock()
|
||||||
|
defer verificationMutex.Unlock()
|
||||||
|
delete(verificationMap, purpose+key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// no lock inside!
|
||||||
|
func removeExpiredPairs() {
|
||||||
|
now := time.Now()
|
||||||
|
for key := range verificationMap {
|
||||||
|
if int(now.Sub(verificationMap[key].time).Seconds()) >= VerificationValidMinutes*60 {
|
||||||
|
delete(verificationMap, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
verificationMutex.Lock()
|
||||||
|
defer verificationMutex.Unlock()
|
||||||
|
verificationMap = make(map[string]verificationValue)
|
||||||
|
}
|
@@ -1,100 +0,0 @@
|
|||||||
const axios = require('axios');
|
|
||||||
const { tokenStore, updateTokenStore } = require('./token');
|
|
||||||
const config = require('../config');
|
|
||||||
|
|
||||||
async function refreshToken() {
|
|
||||||
for (let [key, value] of tokenStore) {
|
|
||||||
if (value.corpId) {
|
|
||||||
value.corpToken = await requestToken(value.corpId, value.corpAppSecret);
|
|
||||||
tokenStore.set(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log('Token refreshed.');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function requestToken(corpId, corpAppSecret) {
|
|
||||||
// Reference: https://work.weixin.qq.com/api/doc/90000/90135/91039
|
|
||||||
|
|
||||||
let token = '';
|
|
||||||
try {
|
|
||||||
let res = await axios.get(
|
|
||||||
`https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${corpId}&corpsecret=${corpAppSecret}`
|
|
||||||
);
|
|
||||||
// console.debug(res);
|
|
||||||
if (res && res.data) {
|
|
||||||
if (res.data.access_token) {
|
|
||||||
token = res.data.access_token;
|
|
||||||
} else {
|
|
||||||
console.error(res.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pushWeChatCorpMessage(userPrefix, message) {
|
|
||||||
// Reference: https://work.weixin.qq.com/api/doc/90000/90135/90236
|
|
||||||
|
|
||||||
let user = tokenStore.get(userPrefix);
|
|
||||||
if (!user) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `不存在的前缀:${userPrefix},请注意大小写`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let access_token = user.corpToken;
|
|
||||||
let request_data = {
|
|
||||||
msgtype: 'textcard',
|
|
||||||
touser: user.corpUserId,
|
|
||||||
agentid: user.corpAgentId,
|
|
||||||
textcard: {
|
|
||||||
title: message.title,
|
|
||||||
description: message.description,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (message.content) {
|
|
||||||
request_data.textcard.url = `${config.href}message/${message.id}`;
|
|
||||||
} else {
|
|
||||||
request_data.textcard.url = `${config.href}`;
|
|
||||||
}
|
|
||||||
let requestUrl = `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${access_token}`;
|
|
||||||
try {
|
|
||||||
let response = await axios.post(requestUrl, request_data);
|
|
||||||
if (response && response.data && response.data.errcode !== 0) {
|
|
||||||
// Failed to push message, get a new token and try again.
|
|
||||||
access_token = await requestToken(user.corpId, user.corpAppSecret);
|
|
||||||
updateTokenStore(userPrefix, 'corpToken', access_token);
|
|
||||||
requestUrl = `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${access_token}`;
|
|
||||||
response = await axios.post(requestUrl, request_data);
|
|
||||||
}
|
|
||||||
if (response.data.errcode === 0) {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: 'ok',
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: response.data.errmsg,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
let msg = e.message;
|
|
||||||
if (msg.startsWith('access_token missing')) {
|
|
||||||
msg = '请求微信服务器失败,请检查配置是否正确或重试!';
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: msg,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
refreshToken,
|
|
||||||
requestToken,
|
|
||||||
pushWeChatCorpMessage,
|
|
||||||
};
|
|
@@ -1,90 +0,0 @@
|
|||||||
const axios = require('axios');
|
|
||||||
const { tokenStore, updateTokenStore } = require('./token');
|
|
||||||
const config = require('../config');
|
|
||||||
|
|
||||||
async function refreshToken() {
|
|
||||||
for (let [key, value] of tokenStore) {
|
|
||||||
if (value.wechatAppId) {
|
|
||||||
value.wechatToken = await requestToken(
|
|
||||||
value.wechatAppId,
|
|
||||||
value.wechatAppSecret
|
|
||||||
);
|
|
||||||
tokenStore.set(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log('Token refreshed.');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function requestToken(appId, appSecret) {
|
|
||||||
let token = '';
|
|
||||||
try {
|
|
||||||
let res = await axios.get(
|
|
||||||
`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}`
|
|
||||||
);
|
|
||||||
// console.debug(res);
|
|
||||||
if (res && res.data) {
|
|
||||||
if (res.data.access_token) {
|
|
||||||
token = res.data.access_token;
|
|
||||||
} else {
|
|
||||||
console.error(res.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pushWeChatMessage(userPrefix, message) {
|
|
||||||
// Reference: https://mp.weixin.qq.com/debug/cgi-bin/readtmpl?t=tmplmsg/faq_tmpl
|
|
||||||
let user = tokenStore.get(userPrefix);
|
|
||||||
if (!user) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `不存在的前缀:${userPrefix},请注意大小写`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let access_token = user.wechatToken;
|
|
||||||
let request_data = {
|
|
||||||
touser: user.wechatOpenId,
|
|
||||||
template_id: user.wechatTemplateId,
|
|
||||||
};
|
|
||||||
if (message.content) {
|
|
||||||
request_data.url = `${config.href}message/${message.id}`;
|
|
||||||
}
|
|
||||||
request_data.data = { text: { value: message.description } };
|
|
||||||
let requestUrl = `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${access_token}`;
|
|
||||||
try {
|
|
||||||
let response = await axios.post(requestUrl, request_data);
|
|
||||||
if (response && response.data && response.data.errcode !== 0) {
|
|
||||||
// Failed to push message, get a new token and try again.
|
|
||||||
let token = await requestToken(user.wechatAppId, user.wechatAppSecret);
|
|
||||||
updateTokenStore(userPrefix, 'wechatToken', token);
|
|
||||||
requestUrl = `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${access_token}`;
|
|
||||||
response = await axios.post(requestUrl, request_data);
|
|
||||||
}
|
|
||||||
if (response.data.errcode === 0) {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: 'ok',
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: response.data.errmsg,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: e.message,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
refreshToken,
|
|
||||||
requestToken,
|
|
||||||
pushWeChatMessage,
|
|
||||||
};
|
|
10
config.js
10
config.js
@@ -1,10 +0,0 @@
|
|||||||
const config = {
|
|
||||||
allowRegister: false,
|
|
||||||
port: process.env.PORT || 3000,
|
|
||||||
database: 'data.db',
|
|
||||||
href: process.env.HREF || 'https://your.domain.com/',
|
|
||||||
session_secret: 'change this',
|
|
||||||
cookie_secret: 'change this',
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = config;
|
|
131
controller/file.go
Normal file
131
controller/file.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"message-pusher/common"
|
||||||
|
"message-pusher/model"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileDeleteRequest struct {
|
||||||
|
Id int
|
||||||
|
Link string
|
||||||
|
//Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
func UploadFile(c *gin.Context) {
|
||||||
|
uploadPath := common.UploadPath
|
||||||
|
//saveToDatabase := true
|
||||||
|
//path := c.PostForm("path")
|
||||||
|
//if path != "" { // Upload to explorer's path
|
||||||
|
// uploadPath = filepath.Join(common.ExplorerRootPath, path)
|
||||||
|
// if !strings.HasPrefix(uploadPath, common.ExplorerRootPath) {
|
||||||
|
// // In this case the given path is not valid, so we reset it to ExplorerRootPath.
|
||||||
|
// uploadPath = common.ExplorerRootPath
|
||||||
|
// }
|
||||||
|
// saveToDatabase = false
|
||||||
|
//}
|
||||||
|
|
||||||
|
description := c.PostForm("description")
|
||||||
|
if description == "" {
|
||||||
|
description = "无描述信息"
|
||||||
|
}
|
||||||
|
uploader := c.GetString("username")
|
||||||
|
if uploader == "" {
|
||||||
|
uploader = "匿名用户"
|
||||||
|
}
|
||||||
|
currentTime := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
form, err := c.MultipartForm()
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
files := form.File["file"]
|
||||||
|
for _, file := range files {
|
||||||
|
// In case someone wants to upload to other folders.
|
||||||
|
filename := filepath.Base(file.Filename)
|
||||||
|
link := filename
|
||||||
|
savePath := filepath.Join(uploadPath, filename)
|
||||||
|
if _, err := os.Stat(savePath); err == nil {
|
||||||
|
// File already existed.
|
||||||
|
t := time.Now()
|
||||||
|
timestamp := t.Format("_2006-01-02_15-04-05")
|
||||||
|
ext := filepath.Ext(filename)
|
||||||
|
if ext == "" {
|
||||||
|
link += timestamp
|
||||||
|
} else {
|
||||||
|
link = filename[:len(filename)-len(ext)] + timestamp + ext
|
||||||
|
}
|
||||||
|
savePath = filepath.Join(uploadPath, link)
|
||||||
|
}
|
||||||
|
if err := c.SaveUploadedFile(file, savePath); err != nil {
|
||||||
|
c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// save to database
|
||||||
|
fileObj := &model.File{
|
||||||
|
Description: description,
|
||||||
|
Uploader: uploader,
|
||||||
|
Time: currentTime,
|
||||||
|
Link: link,
|
||||||
|
Filename: filename,
|
||||||
|
}
|
||||||
|
err = fileObj.Insert()
|
||||||
|
if err != nil {
|
||||||
|
_ = fmt.Errorf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Redirect(http.StatusSeeOther, "./")
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteFile(c *gin.Context) {
|
||||||
|
var deleteRequest FileDeleteRequest
|
||||||
|
err := json.NewDecoder(c.Request.Body).Decode(&deleteRequest)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无效的参数",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileObj := &model.File{
|
||||||
|
Id: deleteRequest.Id,
|
||||||
|
}
|
||||||
|
model.DB.Where("id = ?", deleteRequest.Id).First(&fileObj)
|
||||||
|
err = fileObj.Delete()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
message := "文件删除成功"
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadFile(c *gin.Context) {
|
||||||
|
path := c.Param("file")
|
||||||
|
fullPath := filepath.Join(common.UploadPath, path)
|
||||||
|
if !strings.HasPrefix(fullPath, common.UploadPath) {
|
||||||
|
// We may being attacked!
|
||||||
|
c.Status(403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.File(fullPath)
|
||||||
|
// Update download counter
|
||||||
|
go func() {
|
||||||
|
model.UpdateDownloadCounter(path)
|
||||||
|
}()
|
||||||
|
}
|
179
controller/github.go
Normal file
179
controller/github.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"message-pusher/common"
|
||||||
|
"message-pusher/model"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GitHubOAuthResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GitHubUser struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getGitHubUserInfoByCode(code string) (*GitHubUser, error) {
|
||||||
|
if code == "" {
|
||||||
|
return nil, errors.New("无效的参数")
|
||||||
|
}
|
||||||
|
values := map[string]string{"client_id": common.GitHubClientId, "client_secret": common.GitHubClientSecret, "code": code}
|
||||||
|
jsonData, err := json.Marshal(values)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("POST", "https://github.com/login/oauth/access_token", bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
client := http.Client{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
common.SysLog(err.Error())
|
||||||
|
return nil, errors.New("无法连接至 GitHub 服务器,请稍后重试!")
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
var oAuthResponse GitHubOAuthResponse
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&oAuthResponse)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req, err = http.NewRequest("GET", "https://api.github.com/user", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", oAuthResponse.AccessToken))
|
||||||
|
res2, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
common.SysLog(err.Error())
|
||||||
|
return nil, errors.New("无法连接至 GitHub 服务器,请稍后重试!")
|
||||||
|
}
|
||||||
|
defer res2.Body.Close()
|
||||||
|
var githubUser GitHubUser
|
||||||
|
err = json.NewDecoder(res2.Body).Decode(&githubUser)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if githubUser.Login == "" {
|
||||||
|
return nil, errors.New("返回值非法,用户字段为空,请稍后重试!")
|
||||||
|
}
|
||||||
|
return &githubUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GitHubOAuth(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
username := session.Get("username")
|
||||||
|
if username != nil {
|
||||||
|
GitHubBind(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !common.GitHubOAuthEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员未开启通过 GitHub 登录以及注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := c.Query("code")
|
||||||
|
githubUser, err := getGitHubUserInfoByCode(code)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
GitHubId: githubUser.Login,
|
||||||
|
}
|
||||||
|
if model.IsGitHubIdAlreadyTaken(user.GitHubId) {
|
||||||
|
user.FillUserByGitHubId()
|
||||||
|
} else {
|
||||||
|
user.Username = "github_" + strconv.Itoa(model.GetMaxUserId()+1)
|
||||||
|
user.DisplayName = githubUser.Name
|
||||||
|
user.Email = githubUser.Email
|
||||||
|
user.Role = common.RoleCommonUser
|
||||||
|
user.Status = common.UserStatusEnabled
|
||||||
|
|
||||||
|
if err := user.Insert(); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Status != common.UserStatusEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "用户已被封禁",
|
||||||
|
"success": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setupLogin(&user, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GitHubBind(c *gin.Context) {
|
||||||
|
if !common.GitHubOAuthEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员未开启通过 GitHub 登录以及注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := c.Query("code")
|
||||||
|
githubUser, err := getGitHubUserInfoByCode(code)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
GitHubId: githubUser.Login,
|
||||||
|
}
|
||||||
|
if model.IsGitHubIdAlreadyTaken(user.GitHubId) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "该 GitHub 账户已被绑定",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := c.GetInt("id")
|
||||||
|
user.Id = id
|
||||||
|
user.FillUserById()
|
||||||
|
user.GitHubId = githubUser.Login
|
||||||
|
err = user.Update(false)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "bind",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
167
controller/misc.go
Normal file
167
controller/misc.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"message-pusher/common"
|
||||||
|
"message-pusher/model"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetStatus(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": gin.H{
|
||||||
|
"version": common.Version,
|
||||||
|
"start_time": common.StartTime,
|
||||||
|
"email_verification": common.EmailVerificationEnabled,
|
||||||
|
"github_oauth": common.GitHubOAuthEnabled,
|
||||||
|
"github_client_id": common.GitHubClientId,
|
||||||
|
"system_name": common.SystemName,
|
||||||
|
"footer_html": common.Footer,
|
||||||
|
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
|
||||||
|
"wechat_login": common.WeChatAuthEnabled,
|
||||||
|
"server_address": common.ServerAddress,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNotice(c *gin.Context) {
|
||||||
|
common.OptionMapRWMutex.RLock()
|
||||||
|
defer common.OptionMapRWMutex.RUnlock()
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": common.OptionMap["Notice"],
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAbout(c *gin.Context) {
|
||||||
|
common.OptionMapRWMutex.RLock()
|
||||||
|
defer common.OptionMapRWMutex.RUnlock()
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": common.OptionMap["About"],
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendEmailVerification(c *gin.Context) {
|
||||||
|
email := c.Query("email")
|
||||||
|
if err := common.Validate.Var(email, "required,email"); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无效的参数",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if model.IsEmailAlreadyTaken(email) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "邮箱地址已被占用",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := common.GenerateVerificationCode(6)
|
||||||
|
common.RegisterVerificationCodeWithKey(email, code, common.EmailVerificationPurpose)
|
||||||
|
subject := fmt.Sprintf("%s邮箱验证邮件", common.SystemName)
|
||||||
|
content := fmt.Sprintf("<p>您好,你正在进行%s邮箱验证。</p>"+
|
||||||
|
"<p>您的验证码为: <strong>%s</strong></p>"+
|
||||||
|
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, code, common.VerificationValidMinutes)
|
||||||
|
err := common.SendEmail(subject, email, content)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendPasswordResetEmail(c *gin.Context) {
|
||||||
|
email := c.Query("email")
|
||||||
|
if err := common.Validate.Var(email, "required,email"); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无效的参数",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !model.IsEmailAlreadyTaken(email) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "该邮箱地址未注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := common.GenerateVerificationCode(0)
|
||||||
|
common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose)
|
||||||
|
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", common.ServerAddress, email, code)
|
||||||
|
subject := fmt.Sprintf("%s密码重置", common.SystemName)
|
||||||
|
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
|
||||||
|
"<p>点击<a href='%s'>此处</a>进行密码重置。</p>"+
|
||||||
|
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, common.VerificationValidMinutes)
|
||||||
|
err := common.SendEmail(subject, email, content)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type PasswordResetRequest struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResetPassword(c *gin.Context) {
|
||||||
|
var req PasswordResetRequest
|
||||||
|
err := json.NewDecoder(c.Request.Body).Decode(&req)
|
||||||
|
if req.Email == "" || req.Token == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无效的参数",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !common.VerifyCodeWithKey(req.Email, req.Token, common.PasswordResetPurpose) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "重置链接非法或已过期",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
password := common.GenerateVerificationCode(12)
|
||||||
|
err = model.ResetUserPasswordByEmail(req.Email, password)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.DeleteKey(req.Email, common.PasswordResetPurpose)
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": password,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
69
controller/option.go
Normal file
69
controller/option.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"message-pusher/common"
|
||||||
|
"message-pusher/model"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetOptions(c *gin.Context) {
|
||||||
|
var options []*model.Option
|
||||||
|
common.OptionMapRWMutex.Lock()
|
||||||
|
for k, v := range common.OptionMap {
|
||||||
|
if strings.Contains(k, "Token") || strings.Contains(k, "Secret") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
options = append(options, &model.Option{
|
||||||
|
Key: k,
|
||||||
|
Value: common.Interface2String(v),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
common.OptionMapRWMutex.Unlock()
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": options,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateOption(c *gin.Context) {
|
||||||
|
var option model.Option
|
||||||
|
err := json.NewDecoder(c.Request.Body).Decode(&option)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无效的参数",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if option.Key == "GitHubOAuthEnabled" && option.Value == "true" && common.GitHubClientId == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无法启用 GitHub OAuth,请先填入 GitHub Client ID 以及 GitHub Client Secret!",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
} else if option.Key == "WeChatAuthEnabled" && option.Value == "true" && common.WeChatServerAddress == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无法启用微信登录,请先填入微信登录相关配置信息!",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = model.UpdateOption(option.Key, option.Value)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
583
controller/user.go
Normal file
583
controller/user.go
Normal file
@@ -0,0 +1,583 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"message-pusher/common"
|
||||||
|
"message-pusher/model"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoginRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Login(c *gin.Context) {
|
||||||
|
if !common.PasswordLoginEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "管理员关闭了密码登录",
|
||||||
|
"success": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var loginRequest LoginRequest
|
||||||
|
err := json.NewDecoder(c.Request.Body).Decode(&loginRequest)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "无效的参数",
|
||||||
|
"success": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
username := loginRequest.Username
|
||||||
|
password := loginRequest.Password
|
||||||
|
if username == "" || password == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "无效的参数",
|
||||||
|
"success": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
}
|
||||||
|
err = user.ValidateAndFill()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": err.Error(),
|
||||||
|
"success": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setupLogin(&user, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup session & cookies and then return user info
|
||||||
|
func setupLogin(user *model.User, c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
session.Set("id", user.Id)
|
||||||
|
session.Set("username", user.Username)
|
||||||
|
session.Set("role", user.Role)
|
||||||
|
session.Set("status", user.Status)
|
||||||
|
err := session.Save()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "无法保存会话信息,请重试",
|
||||||
|
"success": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.Password = ""
|
||||||
|
user.Token = ""
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "",
|
||||||
|
"success": true,
|
||||||
|
"data": user,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Logout(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
session.Clear()
|
||||||
|
err := session.Save()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": err.Error(),
|
||||||
|
"success": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "",
|
||||||
|
"success": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Register(c *gin.Context) {
|
||||||
|
if !common.PasswordRegisterEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "管理员关闭了新用户注册",
|
||||||
|
"success": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var user model.User
|
||||||
|
err := json.NewDecoder(c.Request.Body).Decode(&user)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无效的参数",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := common.Validate.Struct(&user); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "输入不合法 " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if common.EmailVerificationEnabled {
|
||||||
|
if user.Email == "" || user.VerificationCode == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员开启了邮箱验证,请输入邮箱地址和验证码",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !common.VerifyCodeWithKey(user.Email, user.VerificationCode, common.EmailVerificationPurpose) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "验证码错误!",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cleanUser := model.User{
|
||||||
|
Username: user.Username,
|
||||||
|
Password: user.Password,
|
||||||
|
DisplayName: user.Username,
|
||||||
|
}
|
||||||
|
if common.EmailVerificationEnabled {
|
||||||
|
cleanUser.Email = user.Email
|
||||||
|
}
|
||||||
|
if err := cleanUser.Insert(); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllUsers(c *gin.Context) {
|
||||||
|
users, err := model.GetAllUsers()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": users,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func SearchUsers(c *gin.Context) {
|
||||||
|
keyword := c.Query("keyword")
|
||||||
|
users, err := model.SearchUsers(keyword)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": users,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUser(c *gin.Context) {
|
||||||
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, err := model.GetUserById(id, false)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
myRole := c.GetInt("role")
|
||||||
|
if myRole <= user.Role {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无权获取同级或更高等级用户的信息",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": user,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateToken(c *gin.Context) {
|
||||||
|
id := c.GetInt("id")
|
||||||
|
user, err := model.GetUserById(id, true)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.Token = uuid.New().String()
|
||||||
|
user.Token = strings.Replace(user.Token, "-", "", -1)
|
||||||
|
|
||||||
|
if model.DB.Where("token = ?", user.Token).First(user).RowsAffected != 0 {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "请重试,系统生成的 UUID 竟然重复了!",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.Update(false); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": user.Token,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSelf(c *gin.Context) {
|
||||||
|
id := c.GetInt("id")
|
||||||
|
user, err := model.GetUserById(id, false)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": user,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateUser(c *gin.Context) {
|
||||||
|
var updatedUser model.User
|
||||||
|
err := json.NewDecoder(c.Request.Body).Decode(&updatedUser)
|
||||||
|
if err != nil || updatedUser.Id == 0 {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无效的参数",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := common.Validate.Struct(&updatedUser); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "输入不合法 " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
originUser, err := model.GetUserById(updatedUser.Id, false)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
myRole := c.GetInt("role")
|
||||||
|
if myRole <= originUser.Role {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无权更新同权限等级或更高权限等级的用户信息",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if myRole <= updatedUser.Role {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无权将其他用户权限等级提升到大于等于自己的权限等级",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updatePassword := updatedUser.Password != ""
|
||||||
|
if err := updatedUser.Update(updatePassword); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateSelf(c *gin.Context) {
|
||||||
|
var user model.User
|
||||||
|
err := json.NewDecoder(c.Request.Body).Decode(&user)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无效的参数",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := common.Validate.Struct(&user); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "输入不合法 " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanUser := model.User{
|
||||||
|
Id: c.GetInt("id"),
|
||||||
|
Username: user.Username,
|
||||||
|
Password: user.Password,
|
||||||
|
DisplayName: user.DisplayName,
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePassword := user.Password != ""
|
||||||
|
if err := cleanUser.Update(updatePassword); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteUser(c *gin.Context) {
|
||||||
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
originUser, err := model.GetUserById(id, false)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
myRole := c.GetInt("role")
|
||||||
|
if myRole <= originUser.Role {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无权删除同权限等级或更高权限等级的用户",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = model.DeleteUserById(id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteSelf(c *gin.Context) {
|
||||||
|
id := c.GetInt("id")
|
||||||
|
err := model.DeleteUserById(id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser Only admin user can call this, so we can trust it
|
||||||
|
func CreateUser(c *gin.Context) {
|
||||||
|
var user model.User
|
||||||
|
err := json.NewDecoder(c.Request.Body).Decode(&user)
|
||||||
|
if err != nil || user.Username == "" || user.Password == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无效的参数",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if user.DisplayName == "" {
|
||||||
|
user.DisplayName = user.Username
|
||||||
|
}
|
||||||
|
myRole := c.GetInt("role")
|
||||||
|
if user.Role >= myRole {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无法创建权限大于等于自己的用户",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.Insert(); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type ManageRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManageUser Only admin user can do this
|
||||||
|
func ManageUser(c *gin.Context) {
|
||||||
|
var req ManageRequest
|
||||||
|
err := json.NewDecoder(c.Request.Body).Decode(&req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无效的参数",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
Username: req.Username,
|
||||||
|
}
|
||||||
|
// Fill attributes
|
||||||
|
model.DB.Where(&user).First(&user)
|
||||||
|
if user.Id == 0 {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "用户不存在",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
myRole := c.GetInt("role")
|
||||||
|
if myRole <= user.Role {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无权更新同权限等级或更高权限等级的用户信息",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch req.Action {
|
||||||
|
case "disable":
|
||||||
|
user.Status = common.UserStatusDisabled
|
||||||
|
case "enable":
|
||||||
|
user.Status = common.UserStatusEnabled
|
||||||
|
case "delete":
|
||||||
|
if err := user.Delete(); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "promote":
|
||||||
|
if myRole != common.RoleRootUser {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "普通管理员用户无法提升其他用户为管理员",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.Role = common.RoleAdminUser
|
||||||
|
case "demote":
|
||||||
|
user.Role = common.RoleCommonUser
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.Update(false); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func EmailBind(c *gin.Context) {
|
||||||
|
email := c.Query("email")
|
||||||
|
code := c.Query("code")
|
||||||
|
if !common.VerifyCodeWithKey(email, code, common.EmailVerificationPurpose) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "验证码错误!",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := c.GetInt("id")
|
||||||
|
user := model.User{
|
||||||
|
Id: id,
|
||||||
|
}
|
||||||
|
user.FillUserById()
|
||||||
|
user.Email = email
|
||||||
|
// no need to check if this email already taken, because we have used verification code to check it
|
||||||
|
err := user.Update(false)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
142
controller/wechat.go
Normal file
142
controller/wechat.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"message-pusher/common"
|
||||||
|
"message-pusher/model"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type wechatLoginResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWeChatIdByCode(code string) (string, error) {
|
||||||
|
if code == "" {
|
||||||
|
return "", errors.New("无效的参数")
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/wechat/user?code=%s", common.WeChatServerAddress, code), nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", common.WeChatServerToken)
|
||||||
|
client := http.Client{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
httpResponse, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer httpResponse.Body.Close()
|
||||||
|
var res wechatLoginResponse
|
||||||
|
err = json.NewDecoder(httpResponse.Body).Decode(&res)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !res.Success {
|
||||||
|
return "", errors.New(res.Message)
|
||||||
|
}
|
||||||
|
if res.Data == "" {
|
||||||
|
return "", errors.New("验证码错误或已过期")
|
||||||
|
}
|
||||||
|
return res.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WeChatAuth(c *gin.Context) {
|
||||||
|
if !common.WeChatAuthEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "管理员未开启通过微信登录以及注册",
|
||||||
|
"success": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := c.Query("code")
|
||||||
|
wechatId, err := getWeChatIdByCode(code)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": err.Error(),
|
||||||
|
"success": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
WeChatId: wechatId,
|
||||||
|
}
|
||||||
|
if model.IsWeChatIdAlreadyTaken(wechatId) {
|
||||||
|
user.FillUserByWeChatId()
|
||||||
|
} else {
|
||||||
|
user.Username = "wechat_" + strconv.Itoa(model.GetMaxUserId()+1)
|
||||||
|
user.DisplayName = "WeChat User"
|
||||||
|
user.Role = common.RoleCommonUser
|
||||||
|
user.Status = common.UserStatusEnabled
|
||||||
|
|
||||||
|
if err := user.Insert(); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Status != common.UserStatusEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "用户已被封禁",
|
||||||
|
"success": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setupLogin(&user, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WeChatBind(c *gin.Context) {
|
||||||
|
if !common.WeChatAuthEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "管理员未开启通过微信登录以及注册",
|
||||||
|
"success": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := c.Query("code")
|
||||||
|
wechatId, err := getWeChatIdByCode(code)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": err.Error(),
|
||||||
|
"success": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if model.IsWeChatIdAlreadyTaken(wechatId) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "该微信账号已被绑定",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := c.GetInt("id")
|
||||||
|
user := model.User{
|
||||||
|
Id: id,
|
||||||
|
}
|
||||||
|
user.FillUserById()
|
||||||
|
user.WeChatId = wechatId
|
||||||
|
err = user.Update(false)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
50
go.mod
Normal file
50
go.mod
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
module message-pusher
|
||||||
|
|
||||||
|
// +heroku goVersion go1.18
|
||||||
|
go 1.18
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-contrib/sessions v0.0.5
|
||||||
|
github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19
|
||||||
|
github.com/gin-gonic/gin v1.8.1
|
||||||
|
github.com/go-playground/validator/v10 v10.11.1
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5
|
||||||
|
github.com/google/uuid v1.3.0
|
||||||
|
golang.org/x/crypto v0.1.0
|
||||||
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||||
|
gorm.io/driver/mysql v1.4.3
|
||||||
|
gorm.io/driver/sqlite v1.4.3
|
||||||
|
gorm.io/gorm v1.24.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.0 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.9.7 // indirect
|
||||||
|
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
||||||
|
github.com/gorilla/context v1.1.1 // indirect
|
||||||
|
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||||
|
github.com/gorilla/sessions v1.2.1 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.1 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
|
||||||
|
github.com/stretchr/testify v1.8.0 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||||
|
golang.org/x/net v0.1.0 // indirect
|
||||||
|
golang.org/x/sys v0.1.0 // indirect
|
||||||
|
golang.org/x/text v0.4.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.28.0 // indirect
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
)
|
143
go.sum
Normal file
143
go.sum
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04=
|
||||||
|
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
|
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
|
||||||
|
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19 h1:J2LPEOcQmWaooBnBtUDV9KHFEnP5LYTZY03GiQ0oQBw=
|
||||||
|
github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg=
|
||||||
|
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
|
||||||
|
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||||
|
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||||
|
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||||
|
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||||
|
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
|
||||||
|
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
|
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
|
||||||
|
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
|
||||||
|
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||||
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||||
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
|
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
|
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||||
|
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||||
|
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||||
|
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||||
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||||
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
|
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
|
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||||
|
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||||
|
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||||
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||||
|
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||||
|
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||||
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||||
|
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
|
||||||
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k=
|
||||||
|
gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
|
||||||
|
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
|
||||||
|
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
||||||
|
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||||
|
gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74=
|
||||||
|
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
84
main.go
Normal file
84
main.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-contrib/sessions/cookie"
|
||||||
|
"github.com/gin-contrib/sessions/redis"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"log"
|
||||||
|
"message-pusher/common"
|
||||||
|
"message-pusher/middleware"
|
||||||
|
"message-pusher/model"
|
||||||
|
"message-pusher/router"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed web/build
|
||||||
|
var buildFS embed.FS
|
||||||
|
|
||||||
|
//go:embed web/build/index.html
|
||||||
|
var indexPage []byte
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
common.SetupGinLog()
|
||||||
|
common.SysLog("system started")
|
||||||
|
if os.Getenv("GIN_MODE") != "debug" {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
// Initialize SQL Database
|
||||||
|
err := model.InitDB()
|
||||||
|
if err != nil {
|
||||||
|
common.FatalLog(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := model.CloseDB()
|
||||||
|
if err != nil {
|
||||||
|
common.FatalLog(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Initialize Redis
|
||||||
|
err = common.InitRedisClient()
|
||||||
|
if err != nil {
|
||||||
|
common.FatalLog(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize options
|
||||||
|
model.InitOptionMap()
|
||||||
|
|
||||||
|
// Initialize HTTP server
|
||||||
|
server := gin.Default()
|
||||||
|
server.Use(middleware.CORS())
|
||||||
|
|
||||||
|
// Initialize session store
|
||||||
|
if common.RedisEnabled {
|
||||||
|
opt := common.ParseRedisOption()
|
||||||
|
store, _ := redis.NewStore(opt.MinIdleConns, opt.Network, opt.Addr, opt.Password, []byte(common.SessionSecret))
|
||||||
|
server.Use(sessions.Sessions("session", store))
|
||||||
|
} else {
|
||||||
|
store := cookie.NewStore([]byte(common.SessionSecret))
|
||||||
|
server.Use(sessions.Sessions("session", store))
|
||||||
|
}
|
||||||
|
|
||||||
|
router.SetRouter(server, buildFS, indexPage)
|
||||||
|
var port = os.Getenv("PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = strconv.Itoa(*common.Port)
|
||||||
|
}
|
||||||
|
//if *common.Host == "localhost" {
|
||||||
|
// ip := common.GetIp()
|
||||||
|
// if ip != "" {
|
||||||
|
// *common.Host = ip
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//serverUrl := "http://" + *common.Host + ":" + port + "/"
|
||||||
|
//if !*common.NoBrowser {
|
||||||
|
// common.OpenBrowser(serverUrl)
|
||||||
|
//}
|
||||||
|
err = server.Run(":" + port)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
117
middleware/auth.go
Normal file
117
middleware/auth.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"message-pusher/common"
|
||||||
|
"message-pusher/model"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func authHelper(c *gin.Context, minRole int) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
username := session.Get("username")
|
||||||
|
role := session.Get("role")
|
||||||
|
id := session.Get("id")
|
||||||
|
status := session.Get("status")
|
||||||
|
authByToken := false
|
||||||
|
if username == nil {
|
||||||
|
// Check token
|
||||||
|
token := c.Request.Header.Get("Authorization")
|
||||||
|
if token == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无权进行此操作,未登录或 token 无效",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.ValidateUserToken(token)
|
||||||
|
if user != nil && user.Username != "" {
|
||||||
|
// Token is valid
|
||||||
|
username = user.Username
|
||||||
|
role = user.Role
|
||||||
|
id = user.Id
|
||||||
|
status = user.Status
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无权进行此操作,未登录或 token 无效",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
authByToken = true
|
||||||
|
}
|
||||||
|
if status.(int) == common.UserStatusDisabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "用户已被封禁",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if role.(int) < minRole {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无权进行此操作,未登录或 token 无效,或没有权限",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Set("username", username)
|
||||||
|
c.Set("role", role)
|
||||||
|
c.Set("id", id)
|
||||||
|
c.Set("authByToken", authByToken)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserAuth() func(c *gin.Context) {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
authHelper(c, common.RoleCommonUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminAuth() func(c *gin.Context) {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
authHelper(c, common.RoleAdminUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RootAuth() func(c *gin.Context) {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
authHelper(c, common.RoleRootUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoTokenAuth You should always use this after normal auth middlewares.
|
||||||
|
func NoTokenAuth() func(c *gin.Context) {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
authByToken := c.GetBool("authByToken")
|
||||||
|
if authByToken {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "本接口不支持使用 token 进行验证",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenOnlyAuth You should always use this after normal auth middlewares.
|
||||||
|
func TokenOnlyAuth() func(c *gin.Context) {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
authByToken := c.GetBool("authByToken")
|
||||||
|
if !authByToken {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "本接口仅支持使用 token 进行验证",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
20
middleware/cors.go
Normal file
20
middleware/cors.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/contrib/cors"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CORS() gin.HandlerFunc {
|
||||||
|
config := cors.DefaultConfig()
|
||||||
|
config.AllowedHeaders = []string{"Authorization", "Content-Type", "Origin",
|
||||||
|
"Connection", "Accept-Encoding", "Accept-Language", "Host"}
|
||||||
|
config.AllowedMethods = []string{"GET", "POST", "DELETE", "OPTIONS", "PUT"}
|
||||||
|
config.AllowCredentials = true
|
||||||
|
config.MaxAge = 12 * time.Hour
|
||||||
|
// if you want to allow all origins, comment the following two lines
|
||||||
|
config.AllowAllOrigins = false
|
||||||
|
config.AllowedOrigins = []string{"https://message-pusher.vercel.app"}
|
||||||
|
return cors.New(config)
|
||||||
|
}
|
103
middleware/rate-limit.go
Normal file
103
middleware/rate-limit.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"message-pusher/common"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var timeFormat = "2006-01-02T15:04:05.000Z"
|
||||||
|
|
||||||
|
var inMemoryRateLimiter common.InMemoryRateLimiter
|
||||||
|
|
||||||
|
func redisRateLimiter(c *gin.Context, maxRequestNum int, duration int64, mark string) {
|
||||||
|
ctx := context.Background()
|
||||||
|
rdb := common.RDB
|
||||||
|
key := "rateLimit:" + mark + c.ClientIP()
|
||||||
|
listLength, err := rdb.LLen(ctx, key).Result()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if listLength < int64(maxRequestNum) {
|
||||||
|
rdb.LPush(ctx, key, time.Now().Format(timeFormat))
|
||||||
|
rdb.Expire(ctx, key, common.RateLimitKeyExpirationDuration)
|
||||||
|
} else {
|
||||||
|
oldTimeStr, _ := rdb.LIndex(ctx, key, -1).Result()
|
||||||
|
oldTime, err := time.Parse(timeFormat, oldTimeStr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nowTimeStr := time.Now().Format(timeFormat)
|
||||||
|
nowTime, err := time.Parse(timeFormat, nowTimeStr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// time.Since will return negative number!
|
||||||
|
// See: https://stackoverflow.com/questions/50970900/why-is-time-since-returning-negative-durations-on-windows
|
||||||
|
if int64(nowTime.Sub(oldTime).Seconds()) < duration {
|
||||||
|
rdb.Expire(ctx, key, common.RateLimitKeyExpirationDuration)
|
||||||
|
c.Status(http.StatusTooManyRequests)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
rdb.LPush(ctx, key, time.Now().Format(timeFormat))
|
||||||
|
rdb.LTrim(ctx, key, 0, int64(maxRequestNum-1))
|
||||||
|
rdb.Expire(ctx, key, common.RateLimitKeyExpirationDuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func memoryRateLimiter(c *gin.Context, maxRequestNum int, duration int64, mark string) {
|
||||||
|
key := mark + c.ClientIP()
|
||||||
|
if !inMemoryRateLimiter.Request(key, maxRequestNum, duration) {
|
||||||
|
c.Status(http.StatusTooManyRequests)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func rateLimitFactory(maxRequestNum int, duration int64, mark string) func(c *gin.Context) {
|
||||||
|
if common.RedisEnabled {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
redisRateLimiter(c, maxRequestNum, duration, mark)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// It's safe to call multi times.
|
||||||
|
inMemoryRateLimiter.Init(common.RateLimitKeyExpirationDuration)
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
memoryRateLimiter(c, maxRequestNum, duration, mark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GlobalWebRateLimit() func(c *gin.Context) {
|
||||||
|
return rateLimitFactory(common.GlobalWebRateLimitNum, common.GlobalWebRateLimitDuration, "GW")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GlobalAPIRateLimit() func(c *gin.Context) {
|
||||||
|
return rateLimitFactory(common.GlobalApiRateLimitNum, common.GlobalApiRateLimitDuration, "GA")
|
||||||
|
}
|
||||||
|
|
||||||
|
func CriticalRateLimit() func(c *gin.Context) {
|
||||||
|
return rateLimitFactory(common.CriticalRateLimitNum, common.CriticalRateLimitDuration, "CT")
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadRateLimit() func(c *gin.Context) {
|
||||||
|
return rateLimitFactory(common.DownloadRateLimitNum, common.DownloadRateLimitDuration, "DW")
|
||||||
|
}
|
||||||
|
|
||||||
|
func UploadRateLimit() func(c *gin.Context) {
|
||||||
|
return rateLimitFactory(common.UploadRateLimitNum, common.UploadRateLimitDuration, "UP")
|
||||||
|
}
|
@@ -1,32 +0,0 @@
|
|||||||
const config = require('../config');
|
|
||||||
|
|
||||||
exports.userRequired = (req, res, next) => {
|
|
||||||
if (req.session.user) {
|
|
||||||
if (req.session.user.isBlocked) {
|
|
||||||
req.flash('message', '用户账户被禁用,请联系管理员');
|
|
||||||
return res.redirect('/');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
req.flash('message', '用户尚未登录,请登录');
|
|
||||||
return res.redirect('/login');
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.adminRequired = (req, res, next) => {
|
|
||||||
if (!req.session.user || !req.session.user.isAdmin) {
|
|
||||||
req.flash('message', '需要管理员权限');
|
|
||||||
return res.redirect('/login');
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.allowRegister = (req, res, next) => {
|
|
||||||
if (!config.allowRegister) {
|
|
||||||
if (!req.session.user || !req.session.user.isAdmin) {
|
|
||||||
req.flash('message', '管理员未开放注册!');
|
|
||||||
return res.redirect('/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
};
|
|
53
model/file.go
Normal file
53
model/file.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"message-pusher/common"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Uploader string `json:"uploader"`
|
||||||
|
Link string `json:"link" gorm:"unique"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
DownloadCounter int `json:"download_counter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllFiles() ([]*File, error) {
|
||||||
|
var files []*File
|
||||||
|
var err error
|
||||||
|
err = DB.Find(&files).Error
|
||||||
|
return files, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func QueryFiles(query string, startIdx int) ([]*File, error) {
|
||||||
|
var files []*File
|
||||||
|
var err error
|
||||||
|
query = strings.ToLower(query)
|
||||||
|
err = DB.Limit(common.ItemsPerPage).Offset(startIdx).Where("filename LIKE ? or description LIKE ? or uploader LIKE ? or time LIKE ?", "%"+query+"%", "%"+query+"%", "%"+query+"%", "%"+query+"%").Order("id desc").Find(&files).Error
|
||||||
|
return files, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (file *File) Insert() error {
|
||||||
|
var err error
|
||||||
|
err = DB.Create(file).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete Make sure link is valid! Because we will use os.Remove to delete it!
|
||||||
|
func (file *File) Delete() error {
|
||||||
|
var err error
|
||||||
|
err = DB.Delete(file).Error
|
||||||
|
err = os.Remove(path.Join(common.UploadPath, file.Link))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateDownloadCounter(link string) {
|
||||||
|
DB.Model(&File{}).Where("link = ?", link).UpdateColumn("download_counter", gorm.Expr("download_counter + 1"))
|
||||||
|
}
|
81
model/main.go
Normal file
81
model/main.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"message-pusher/common"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DB *gorm.DB
|
||||||
|
|
||||||
|
func createRootAccountIfNeed() error {
|
||||||
|
var user User
|
||||||
|
//if user.Status != common.UserStatusEnabled {
|
||||||
|
if err := DB.First(&user).Error; err != nil {
|
||||||
|
hashedPassword, err := common.Password2Hash("123456")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rootUser := User{
|
||||||
|
Username: "root",
|
||||||
|
Password: hashedPassword,
|
||||||
|
Role: common.RoleRootUser,
|
||||||
|
Status: common.UserStatusEnabled,
|
||||||
|
DisplayName: "Root User",
|
||||||
|
}
|
||||||
|
DB.Create(&rootUser)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CountTable(tableName string) (num int64) {
|
||||||
|
DB.Table(tableName).Count(&num)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitDB() (err error) {
|
||||||
|
var db *gorm.DB
|
||||||
|
if os.Getenv("SQL_DSN") != "" {
|
||||||
|
// Use MySQL
|
||||||
|
db, err = gorm.Open(mysql.Open(os.Getenv("SQL_DSN")), &gorm.Config{
|
||||||
|
PrepareStmt: true, // precompile SQL
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Use SQLite
|
||||||
|
db, err = gorm.Open(sqlite.Open(common.SQLitePath), &gorm.Config{
|
||||||
|
PrepareStmt: true, // precompile SQL
|
||||||
|
})
|
||||||
|
common.SysLog("SQL_DSN not set, using SQLite as database")
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
DB = db
|
||||||
|
err := db.AutoMigrate(&File{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.AutoMigrate(&User{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.AutoMigrate(&Option{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = createRootAccountIfNeed()
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
common.FatalLog(err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloseDB() error {
|
||||||
|
sqlDB, err := DB.DB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = sqlDB.Close()
|
||||||
|
return err
|
||||||
|
}
|
123
model/option.go
Normal file
123
model/option.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"message-pusher/common"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Option struct {
|
||||||
|
Key string `json:"key" gorm:"primaryKey"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func AllOption() ([]*Option, error) {
|
||||||
|
var options []*Option
|
||||||
|
var err error
|
||||||
|
err = DB.Find(&options).Error
|
||||||
|
return options, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitOptionMap() {
|
||||||
|
common.OptionMapRWMutex.Lock()
|
||||||
|
common.OptionMap = make(map[string]string)
|
||||||
|
common.OptionMap["FileUploadPermission"] = strconv.Itoa(common.FileUploadPermission)
|
||||||
|
common.OptionMap["FileDownloadPermission"] = strconv.Itoa(common.FileDownloadPermission)
|
||||||
|
common.OptionMap["ImageUploadPermission"] = strconv.Itoa(common.ImageUploadPermission)
|
||||||
|
common.OptionMap["ImageDownloadPermission"] = strconv.Itoa(common.ImageDownloadPermission)
|
||||||
|
common.OptionMap["PasswordLoginEnabled"] = strconv.FormatBool(common.PasswordLoginEnabled)
|
||||||
|
common.OptionMap["PasswordRegisterEnabled"] = strconv.FormatBool(common.PasswordRegisterEnabled)
|
||||||
|
common.OptionMap["EmailVerificationEnabled"] = strconv.FormatBool(common.EmailVerificationEnabled)
|
||||||
|
common.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(common.GitHubOAuthEnabled)
|
||||||
|
common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled)
|
||||||
|
common.OptionMap["SMTPServer"] = ""
|
||||||
|
common.OptionMap["SMTPAccount"] = ""
|
||||||
|
common.OptionMap["SMTPToken"] = ""
|
||||||
|
common.OptionMap["Notice"] = ""
|
||||||
|
common.OptionMap["About"] = ""
|
||||||
|
common.OptionMap["Footer"] = common.Footer
|
||||||
|
common.OptionMap["ServerAddress"] = ""
|
||||||
|
common.OptionMap["GitHubClientId"] = ""
|
||||||
|
common.OptionMap["GitHubClientSecret"] = ""
|
||||||
|
common.OptionMap["WeChatServerAddress"] = ""
|
||||||
|
common.OptionMap["WeChatServerToken"] = ""
|
||||||
|
common.OptionMap["WeChatAccountQRCodeImageURL"] = ""
|
||||||
|
common.OptionMapRWMutex.Unlock()
|
||||||
|
options, _ := AllOption()
|
||||||
|
for _, option := range options {
|
||||||
|
updateOptionMap(option.Key, option.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateOption(key string, value string) error {
|
||||||
|
if key == "StatEnabled" && value == "true" && !common.RedisEnabled {
|
||||||
|
return errors.New("未启用 Redis,无法启用统计功能")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to database first
|
||||||
|
option := Option{
|
||||||
|
Key: key,
|
||||||
|
Value: value,
|
||||||
|
}
|
||||||
|
// When updating with struct it will only update non-zero fields by default
|
||||||
|
// So we have to use Select here
|
||||||
|
if DB.Model(&option).Where("key = ?", key).Update("value", option.Value).RowsAffected == 0 {
|
||||||
|
DB.Create(&option)
|
||||||
|
}
|
||||||
|
// Update OptionMap
|
||||||
|
updateOptionMap(key, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateOptionMap(key string, value string) {
|
||||||
|
common.OptionMapRWMutex.Lock()
|
||||||
|
defer common.OptionMapRWMutex.Unlock()
|
||||||
|
common.OptionMap[key] = value
|
||||||
|
if strings.HasSuffix(key, "Permission") {
|
||||||
|
intValue, _ := strconv.Atoi(value)
|
||||||
|
switch key {
|
||||||
|
case "FileUploadPermission":
|
||||||
|
common.FileUploadPermission = intValue
|
||||||
|
case "FileDownloadPermission":
|
||||||
|
common.FileDownloadPermission = intValue
|
||||||
|
case "ImageUploadPermission":
|
||||||
|
common.ImageUploadPermission = intValue
|
||||||
|
case "ImageDownloadPermission":
|
||||||
|
common.ImageDownloadPermission = intValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolValue := value == "true"
|
||||||
|
switch key {
|
||||||
|
case "PasswordRegisterEnabled":
|
||||||
|
common.PasswordRegisterEnabled = boolValue
|
||||||
|
case "PasswordLoginEnabled":
|
||||||
|
common.PasswordLoginEnabled = boolValue
|
||||||
|
case "EmailVerificationEnabled":
|
||||||
|
common.EmailVerificationEnabled = boolValue
|
||||||
|
case "GitHubOAuthEnabled":
|
||||||
|
common.GitHubOAuthEnabled = boolValue
|
||||||
|
case "SMTPServer":
|
||||||
|
common.SMTPServer = value
|
||||||
|
case "SMTPAccount":
|
||||||
|
common.SMTPAccount = value
|
||||||
|
case "SMTPToken":
|
||||||
|
common.SMTPToken = value
|
||||||
|
case "ServerAddress":
|
||||||
|
common.ServerAddress = value
|
||||||
|
case "GitHubClientId":
|
||||||
|
common.GitHubClientId = value
|
||||||
|
case "GitHubClientSecret":
|
||||||
|
common.GitHubClientSecret = value
|
||||||
|
case "Footer":
|
||||||
|
common.Footer = value
|
||||||
|
case "WeChatServerAddress":
|
||||||
|
common.WeChatServerAddress = value
|
||||||
|
case "WeChatServerToken":
|
||||||
|
common.WeChatServerToken = value
|
||||||
|
case "WeChatAccountQRCodeImageURL":
|
||||||
|
common.WeChatAccountQRCodeImageURL = value
|
||||||
|
case "WeChatAuthEnabled":
|
||||||
|
common.WeChatAuthEnabled = boolValue
|
||||||
|
}
|
||||||
|
}
|
155
model/user.go
Normal file
155
model/user.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"message-pusher/common"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Username string `json:"username" gorm:"unique;index" validate:"max=12"`
|
||||||
|
Password string `json:"password" gorm:"not null;" validate:"min=8,max=20"`
|
||||||
|
DisplayName string `json:"display_name" gorm:"index" validate:"max=20"`
|
||||||
|
Role int `json:"role" gorm:"type:int;default:1"` // admin, common
|
||||||
|
Status int `json:"status" gorm:"type:int;default:1"` // enabled, disabled
|
||||||
|
Token string `json:"token;" gorm:"index"`
|
||||||
|
Email string `json:"email" gorm:"index" validate:"max=50"`
|
||||||
|
GitHubId string `json:"github_id" gorm:"column:github_id;index"`
|
||||||
|
WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"`
|
||||||
|
VerificationCode string `json:"verification_code" gorm:"-:all"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMaxUserId() int {
|
||||||
|
var user User
|
||||||
|
DB.Last(&user)
|
||||||
|
return user.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllUsers() (users []*User, err error) {
|
||||||
|
err = DB.Select([]string{"id", "username", "display_name", "role", "status", "email"}).Find(&users).Error
|
||||||
|
return users, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func SearchUsers(keyword string) (users []*User, err error) {
|
||||||
|
err = DB.Select([]string{"id", "username", "display_name", "role", "status", "email"}).Where("id = ? or username LIKE ? or email LIKE ? or display_name LIKE ?", keyword, keyword+"%", keyword+"%", keyword+"%").Find(&users).Error
|
||||||
|
return users, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserById(id int, selectAll bool) (*User, error) {
|
||||||
|
user := User{Id: id}
|
||||||
|
var err error = nil
|
||||||
|
if selectAll {
|
||||||
|
err = DB.First(&user, "id = ?", id).Error
|
||||||
|
} else {
|
||||||
|
err = DB.Select([]string{"id", "username", "display_name", "role", "status", "email", "wechat_id", "github_id"}).First(&user, "id = ?", id).Error
|
||||||
|
}
|
||||||
|
return &user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteUserById(id int) (err error) {
|
||||||
|
user := User{Id: id}
|
||||||
|
err = DB.Delete(&user).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) Insert() error {
|
||||||
|
var err error
|
||||||
|
if user.Password != "" {
|
||||||
|
user.Password, err = common.Password2Hash(user.Password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = DB.Create(user).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) Update(updatePassword bool) error {
|
||||||
|
var err error
|
||||||
|
if updatePassword {
|
||||||
|
user.Password, err = common.Password2Hash(user.Password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = DB.Model(user).Updates(user).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) Delete() error {
|
||||||
|
var err error
|
||||||
|
err = DB.Delete(user).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAndFill check password & user status
|
||||||
|
func (user *User) ValidateAndFill() (err error) {
|
||||||
|
// When querying with struct, GORM will only query with non-zero fields,
|
||||||
|
// that means if your field’s value is 0, '', false or other zero values,
|
||||||
|
// it won’t be used to build query conditions
|
||||||
|
password := user.Password
|
||||||
|
DB.Where(User{Username: user.Username}).First(user)
|
||||||
|
okay := common.ValidatePasswordAndHash(password, user.Password)
|
||||||
|
if !okay || user.Status != common.UserStatusEnabled {
|
||||||
|
return errors.New("用户名或密码错误,或者该用户已被封禁")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) FillUserById() {
|
||||||
|
DB.Where(User{Id: user.Id}).First(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) FillUserByEmail() {
|
||||||
|
DB.Where(User{Email: user.Email}).First(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) FillUserByGitHubId() {
|
||||||
|
DB.Where(User{GitHubId: user.GitHubId}).First(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) FillUserByWeChatId() {
|
||||||
|
DB.Where(User{WeChatId: user.WeChatId}).First(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) FillUserByUsername() {
|
||||||
|
DB.Where(User{Username: user.Username}).First(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateUserToken(token string) (user *User) {
|
||||||
|
if token == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
token = strings.Replace(token, "Bearer ", "", 1)
|
||||||
|
user = &User{}
|
||||||
|
if DB.Where("token = ?", token).First(user).RowsAffected == 1 {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsEmailAlreadyTaken(email string) bool {
|
||||||
|
return DB.Where("email = ?", email).Find(&User{}).RowsAffected == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsWeChatIdAlreadyTaken(wechatId string) bool {
|
||||||
|
return DB.Where("wechat_id = ?", wechatId).Find(&User{}).RowsAffected == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsGitHubIdAlreadyTaken(githubId string) bool {
|
||||||
|
return DB.Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsUsernameAlreadyTaken(username string) bool {
|
||||||
|
return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResetUserPasswordByEmail(email string, password string) error {
|
||||||
|
hashedPassword, err := common.Password2Hash(password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = DB.Model(&User{}).Where("email = ?", email).Update("password", hashedPassword).Error
|
||||||
|
return err
|
||||||
|
}
|
@@ -1,24 +0,0 @@
|
|||||||
const User = require('./user');
|
|
||||||
const Message = require('./message');
|
|
||||||
const sequelize = require('../common/database');
|
|
||||||
|
|
||||||
Message.belongsTo(User);
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
await sequelize.sync();
|
|
||||||
console.log('Database initialized.');
|
|
||||||
const isNoAdminExisted =
|
|
||||||
(await User.findOne({ where: { isAdmin: true } })) === null;
|
|
||||||
if (isNoAdminExisted) {
|
|
||||||
console.log('No admin user existed! Creating one for you.');
|
|
||||||
await User.create({
|
|
||||||
username: 'admin',
|
|
||||||
password: '123456',
|
|
||||||
isAdmin: true,
|
|
||||||
prefix: 'admin',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
exports.User = User;
|
|
||||||
exports.Message = Message;
|
|
@@ -1,24 +0,0 @@
|
|||||||
const { DataTypes, Model } = require('sequelize');
|
|
||||||
const sequelize = require('../common/database');
|
|
||||||
|
|
||||||
class Message extends Model {}
|
|
||||||
|
|
||||||
Message.init(
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
title: DataTypes.STRING,
|
|
||||||
description: DataTypes.TEXT,
|
|
||||||
content: DataTypes.TEXT,
|
|
||||||
type: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
defaultValue: 'test',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ sequelize }
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = Message;
|
|
@@ -1,66 +0,0 @@
|
|||||||
const { DataTypes, Model } = require('sequelize');
|
|
||||||
const sequelize = require('../common/database');
|
|
||||||
|
|
||||||
class User extends Model {}
|
|
||||||
|
|
||||||
User.init(
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
username: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
unique: true,
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
accessToken: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
defaultValue: '',
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
},
|
|
||||||
prefix: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
unique: true,
|
|
||||||
},
|
|
||||||
isAdmin: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
},
|
|
||||||
isBlocked: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
},
|
|
||||||
defaultMethod:{
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
defaultValue: 'test'
|
|
||||||
},
|
|
||||||
// WeChat public account
|
|
||||||
wechatAppId: DataTypes.STRING,
|
|
||||||
wechatAppSecret: DataTypes.STRING,
|
|
||||||
wechatTemplateId: DataTypes.STRING,
|
|
||||||
wechatOpenId: DataTypes.STRING,
|
|
||||||
wechatVerifyToken: DataTypes.STRING,
|
|
||||||
// Email
|
|
||||||
smtpServer: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
defaultValue: 'smtp.qq.com',
|
|
||||||
},
|
|
||||||
smtpUser: DataTypes.STRING,
|
|
||||||
smtpPass: DataTypes.STRING,
|
|
||||||
// WeChat corporation
|
|
||||||
corpId: DataTypes.STRING,
|
|
||||||
corpAgentId: DataTypes.STRING,
|
|
||||||
corpAppSecret: DataTypes.STRING,
|
|
||||||
corpUserId: DataTypes.STRING
|
|
||||||
},
|
|
||||||
{ sequelize }
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = User;
|
|
15
nginx.conf
15
nginx.conf
@@ -1,15 +0,0 @@
|
|||||||
server {
|
|
||||||
server_name 你的域名;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:3000; # 注意如果你改变了默认端口,记得在这里进行更新
|
|
||||||
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-Forwarded-For $remote_addr;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
proxy_send_timeout 300s;
|
|
||||||
}
|
|
||||||
}
|
|
35
package.json
35
package.json
@@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "message-pusher",
|
|
||||||
"version": "0.2.3",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"start": "node ./app.js",
|
|
||||||
"devStart": "nodemon ./app.js"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"axios": "^0.21.1",
|
|
||||||
"compression": "^1.7.4",
|
|
||||||
"connect-flash": "^0.1.1",
|
|
||||||
"cookie-parser": "~1.4.4",
|
|
||||||
"debug": "~2.6.9",
|
|
||||||
"dotenv": "^16.0.1",
|
|
||||||
"ejs": "^3.1.5",
|
|
||||||
"express": "~4.16.1",
|
|
||||||
"express-rate-limit": "^5.2.3",
|
|
||||||
"express-session": "^1.17.1",
|
|
||||||
"marked": ">=2.0.0",
|
|
||||||
"morgan": "~1.9.1",
|
|
||||||
"nodemailer": "^6.4.17",
|
|
||||||
"sequelize": "^6.3.5",
|
|
||||||
"serve-static": "^1.14.1",
|
|
||||||
"sqlite3": "^5.0.1",
|
|
||||||
"ws": "^7.4.6"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"nodemon": "^2.0.6",
|
|
||||||
"prettier": "^2.1.1"
|
|
||||||
},
|
|
||||||
"prettier": {
|
|
||||||
"singleQuote": true
|
|
||||||
}
|
|
||||||
}
|
|
1
public/bulma.min.css
vendored
1
public/bulma.min.css
vendored
File diff suppressed because one or more lines are too long
246
public/main.css
246
public/main.css
@@ -1,246 +0,0 @@
|
|||||||
body {
|
|
||||||
font-family: Verdana, Candara, Arial, Helvetica, Microsoft YaHei, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
max-width: 960px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-container {
|
|
||||||
position: relative;
|
|
||||||
min-height: 97vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-wrap {
|
|
||||||
padding-bottom: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer {
|
|
||||||
height: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer a {
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: Consolas, 'Courier New', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-card-list {
|
|
||||||
margin: 8px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-card-title {
|
|
||||||
font-size: x-large;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #000000;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-card-text {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination {
|
|
||||||
margin: 16px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination a {
|
|
||||||
border: none;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadow {
|
|
||||||
box-shadow: 0 0.5em 1em -0.125em rgba(10,10,10,.1), 0 0 0 1px rgba(10,10,10,.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-shadow {
|
|
||||||
box-shadow: 0 2px 3px rgba(26,26,26,.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.box .article {
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
font-size: larger;
|
|
||||||
word-break: break-word;
|
|
||||||
line-height: 1.6;
|
|
||||||
padding: 16px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.normal-container {
|
|
||||||
margin: auto;
|
|
||||||
max-width: 960px;
|
|
||||||
padding: 16px 16px;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
word-break: break-word;
|
|
||||||
line-height: 1.6;
|
|
||||||
font-size: larger;
|
|
||||||
}
|
|
||||||
|
|
||||||
.narrow-container {
|
|
||||||
margin: auto;
|
|
||||||
max-width: 560px;
|
|
||||||
padding: 16px 16px;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
word-break: break-word;
|
|
||||||
line-height: 1.6;
|
|
||||||
font-size: larger;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article a {
|
|
||||||
color: #368CCB;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article a:hover {
|
|
||||||
color: #368CCB;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article h2,
|
|
||||||
.article h3,
|
|
||||||
.article h4,
|
|
||||||
.article h5,
|
|
||||||
.article h6 {
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.5;
|
|
||||||
margin: 20px 0 15px;
|
|
||||||
margin-block-start: 1em;
|
|
||||||
margin-block-end: 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article h1 {
|
|
||||||
font-size: 1.7em
|
|
||||||
}
|
|
||||||
|
|
||||||
.article h2 {
|
|
||||||
font-size: 1.6em
|
|
||||||
}
|
|
||||||
|
|
||||||
.article h3 {
|
|
||||||
font-size: 1.45em
|
|
||||||
}
|
|
||||||
|
|
||||||
.article h4 {
|
|
||||||
font-size: 1.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article h5 {
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
.article h6 {
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: bold
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 960px) {
|
|
||||||
.article h1 {
|
|
||||||
font-size: 1.5em
|
|
||||||
}
|
|
||||||
|
|
||||||
.article h2 {
|
|
||||||
font-size: 1.35em
|
|
||||||
}
|
|
||||||
|
|
||||||
.article h3 {
|
|
||||||
font-size: 1.3em
|
|
||||||
}
|
|
||||||
|
|
||||||
.article h4 {
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.article p {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article table {
|
|
||||||
margin: auto;
|
|
||||||
border-collapse: collapse;
|
|
||||||
border-spacing: 0;
|
|
||||||
vertical-align: middle;
|
|
||||||
text-align: left;
|
|
||||||
min-width: 66%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article table td,
|
|
||||||
.article table th {
|
|
||||||
padding: 5px 8px;
|
|
||||||
border: 1px solid #bbb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article blockquote {
|
|
||||||
margin-left: 0;
|
|
||||||
padding: 0 1em;
|
|
||||||
font-size: smaller;
|
|
||||||
border-left: 5px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article pre {
|
|
||||||
overflow-x: auto;
|
|
||||||
padding: 0;
|
|
||||||
font-size: 16px;
|
|
||||||
margin-top: 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article ol {
|
|
||||||
text-decoration: none;
|
|
||||||
padding-inline-start: 40px;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article code {
|
|
||||||
color: #bc9458;
|
|
||||||
padding: .065em .4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article .copyright{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 28px;
|
|
||||||
text-align: left;
|
|
||||||
color: #738292;
|
|
||||||
margin-bottom: 3em
|
|
||||||
}
|
|
||||||
|
|
||||||
.info a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links {
|
|
||||||
margin: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.line {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toc {
|
|
||||||
position: sticky;
|
|
||||||
top: 24px;
|
|
||||||
}
|
|
@@ -1,2 +0,0 @@
|
|||||||
User-agent: *
|
|
||||||
Disallow: /
|
|
64
router/api-router.go
Normal file
64
router/api-router.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"message-pusher/controller"
|
||||||
|
"message-pusher/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetApiRouter(router *gin.Engine) {
|
||||||
|
apiRouter := router.Group("/api")
|
||||||
|
apiRouter.Use(middleware.GlobalAPIRateLimit())
|
||||||
|
{
|
||||||
|
apiRouter.GET("/status", controller.GetStatus)
|
||||||
|
apiRouter.GET("/notice", controller.GetNotice)
|
||||||
|
apiRouter.GET("/about", controller.GetAbout)
|
||||||
|
apiRouter.GET("/verification", middleware.CriticalRateLimit(), controller.SendEmailVerification)
|
||||||
|
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), controller.SendPasswordResetEmail)
|
||||||
|
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
||||||
|
apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), controller.GitHubOAuth)
|
||||||
|
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
|
||||||
|
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)
|
||||||
|
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind)
|
||||||
|
|
||||||
|
userRoute := apiRouter.Group("/user")
|
||||||
|
{
|
||||||
|
userRoute.POST("/register", middleware.CriticalRateLimit(), controller.Register)
|
||||||
|
userRoute.POST("/login", middleware.CriticalRateLimit(), controller.Login)
|
||||||
|
userRoute.GET("/logout", controller.Logout)
|
||||||
|
|
||||||
|
selfRoute := userRoute.Group("/")
|
||||||
|
selfRoute.Use(middleware.UserAuth(), middleware.NoTokenAuth())
|
||||||
|
{
|
||||||
|
selfRoute.GET("/self", controller.GetSelf)
|
||||||
|
selfRoute.PUT("/self", controller.UpdateSelf)
|
||||||
|
selfRoute.DELETE("/self", controller.DeleteSelf)
|
||||||
|
selfRoute.GET("/token", controller.GenerateToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
adminRoute := userRoute.Group("/")
|
||||||
|
adminRoute.Use(middleware.AdminAuth(), middleware.NoTokenAuth())
|
||||||
|
{
|
||||||
|
adminRoute.GET("/", controller.GetAllUsers)
|
||||||
|
adminRoute.GET("/search", controller.SearchUsers)
|
||||||
|
adminRoute.GET("/:id", controller.GetUser)
|
||||||
|
adminRoute.POST("/", controller.CreateUser)
|
||||||
|
adminRoute.POST("/manage", controller.ManageUser)
|
||||||
|
adminRoute.PUT("/", controller.UpdateUser)
|
||||||
|
adminRoute.DELETE("/:id", controller.DeleteUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
optionRoute := apiRouter.Group("/option")
|
||||||
|
optionRoute.Use(middleware.RootAuth(), middleware.NoTokenAuth())
|
||||||
|
{
|
||||||
|
optionRoute.GET("/", controller.GetOptions)
|
||||||
|
optionRoute.PUT("/", controller.UpdateOption)
|
||||||
|
}
|
||||||
|
fileRoute := apiRouter.Group("/file")
|
||||||
|
{
|
||||||
|
fileRoute.GET("/:id", middleware.DownloadRateLimit(), controller.DownloadFile)
|
||||||
|
fileRoute.POST("/", middleware.UserAuth(), middleware.UploadRateLimit(), controller.UploadFile)
|
||||||
|
fileRoute.DELETE("/:id", middleware.UserAuth(), controller.DeleteFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
router/main.go
Normal file
11
router/main.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
|
||||||
|
SetApiRouter(router)
|
||||||
|
setWebRouter(router, buildFS, indexPage)
|
||||||
|
}
|
18
router/web-router.go
Normal file
18
router/web-router.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"github.com/gin-gonic/contrib/static"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"message-pusher/common"
|
||||||
|
"message-pusher/middleware"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
|
||||||
|
router.Use(middleware.GlobalWebRateLimit())
|
||||||
|
router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build")))
|
||||||
|
router.NoRoute(func(c *gin.Context) {
|
||||||
|
c.Data(http.StatusOK, "text/html; charset=utf-8", indexPage)
|
||||||
|
})
|
||||||
|
}
|
209
routers/index.js
209
routers/index.js
@@ -1,209 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const { User } = require('../models');
|
|
||||||
const { tokenStore } = require('../common/token');
|
|
||||||
const requestWeChatToken = require('../common/wechat').requestToken;
|
|
||||||
const requestCorpToken = require('../common/wechat-corp').requestToken;
|
|
||||||
|
|
||||||
const {
|
|
||||||
userRequired,
|
|
||||||
adminRequired,
|
|
||||||
allowRegister,
|
|
||||||
} = require('../middlewares/web_auth');
|
|
||||||
const config = require('../config');
|
|
||||||
|
|
||||||
router.get('/', (req, res, next) => {
|
|
||||||
let showGuidance = false;
|
|
||||||
if (
|
|
||||||
req.session.user &&
|
|
||||||
!req.session.user.wechatAppId &&
|
|
||||||
!req.session.user.corpId &&
|
|
||||||
!req.session.user.smtpUser
|
|
||||||
) {
|
|
||||||
showGuidance = true;
|
|
||||||
}
|
|
||||||
if (process.env.MODE === '1') {
|
|
||||||
showGuidance = false;
|
|
||||||
}
|
|
||||||
res.render('index', {
|
|
||||||
message: req.flash('message'),
|
|
||||||
showGuidance,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/login', (req, res, next) => {
|
|
||||||
res.render('login', {
|
|
||||||
message: req.flash('message'),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/login', async (req, res, next) => {
|
|
||||||
if (process.env.MODE === '1') {
|
|
||||||
return res.render('register', {
|
|
||||||
message: '当前运行模式为 Heroku 模式,该模式下禁止用户登录',
|
|
||||||
isErrorMessage: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let user = {
|
|
||||||
username: req.body.username,
|
|
||||||
password: req.body.password,
|
|
||||||
};
|
|
||||||
let message = '';
|
|
||||||
res.locals.isErrorMessage = true;
|
|
||||||
try {
|
|
||||||
user = await User.findOne({ where: user });
|
|
||||||
if (user) {
|
|
||||||
req.session.user = user;
|
|
||||||
req.flash(
|
|
||||||
'message',
|
|
||||||
`欢迎${user.isAdmin ? '管理员' : '普通'}用户 ${
|
|
||||||
user.username
|
|
||||||
} 登录系统!`
|
|
||||||
);
|
|
||||||
return res.redirect('/');
|
|
||||||
} else {
|
|
||||||
message = '用户名或密码错误';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
message = e.message;
|
|
||||||
}
|
|
||||||
res.render('login', {
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/logout', userRequired, (req, res, next) => {
|
|
||||||
req.session.user = undefined;
|
|
||||||
req.flash('message', '已退出登录');
|
|
||||||
res.redirect('/');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/register', allowRegister, (req, res, next) => {
|
|
||||||
res.render('register');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/register', allowRegister, async (req, res, next) => {
|
|
||||||
if (process.env.MODE === '1') {
|
|
||||||
return res.render('register', {
|
|
||||||
message: '当前运行模式为 Heroku 模式,该模式下禁止用户注册',
|
|
||||||
isErrorMessage: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let user = {
|
|
||||||
username: req.body.username,
|
|
||||||
password: req.body.password,
|
|
||||||
};
|
|
||||||
let message = '';
|
|
||||||
try {
|
|
||||||
user = await User.create(user);
|
|
||||||
message = '用户创建成功,请登录';
|
|
||||||
req.flash('message', message);
|
|
||||||
return res.redirect('/login');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
message = '用户名已被占用';
|
|
||||||
}
|
|
||||||
res.render('register', { message, isErrorMessage: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/configure', userRequired, (req, res, next) => {
|
|
||||||
let showPasswordWarning = false;
|
|
||||||
if (req.session.user && req.session.user.password === '123456') {
|
|
||||||
showPasswordWarning = true;
|
|
||||||
}
|
|
||||||
res.locals.message = req.flash('message');
|
|
||||||
res.locals.showPasswordWarning = showPasswordWarning;
|
|
||||||
if (req.session.user.prefix === null) {
|
|
||||||
req.session.user.prefix = req.session.user.username;
|
|
||||||
}
|
|
||||||
res.locals.verifyUrl = config.href + req.session.user.prefix + '/verify';
|
|
||||||
res.render('configure', req.session.user);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/configure', userRequired, async (req, res, next) => {
|
|
||||||
let id = req.session.user.id;
|
|
||||||
let user = {
|
|
||||||
// Common
|
|
||||||
username: req.body.username,
|
|
||||||
password: req.body.password,
|
|
||||||
accessToken: req.body.accessToken,
|
|
||||||
defaultMethod: req.body.defaultMethod,
|
|
||||||
prefix: req.body.prefix,
|
|
||||||
// WeChat public account
|
|
||||||
wechatAppId: req.body.wechatAppId,
|
|
||||||
wechatAppSecret: req.body.wechatAppSecret,
|
|
||||||
wechatTemplateId: req.body.wechatTemplateId,
|
|
||||||
wechatOpenId: req.body.wechatOpenId,
|
|
||||||
wechatVerifyToken: req.body.wechatVerifyToken,
|
|
||||||
// Email
|
|
||||||
email: req.body.email,
|
|
||||||
smtpServer: req.body.smtpServer,
|
|
||||||
smtpUser: req.body.smtpUser,
|
|
||||||
smtpPass: req.body.smtpPass,
|
|
||||||
// WeChat corp
|
|
||||||
corpId: req.body.corpId,
|
|
||||||
corpAgentId: req.body.corpAgentId,
|
|
||||||
corpAppSecret: req.body.corpAppSecret,
|
|
||||||
corpUserId: req.body.corpUserId,
|
|
||||||
};
|
|
||||||
for (let field in user) {
|
|
||||||
let value = user[field];
|
|
||||||
value = value.trim();
|
|
||||||
if (value || field === 'accessToken') {
|
|
||||||
user[field] = value;
|
|
||||||
} else {
|
|
||||||
delete user[field];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let message = '';
|
|
||||||
try {
|
|
||||||
let userObj = await User.findOne({
|
|
||||||
where: {
|
|
||||||
id: id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (userObj) {
|
|
||||||
await userObj.update(user);
|
|
||||||
}
|
|
||||||
if (userObj.prefix !== req.session.user.prefix) {
|
|
||||||
tokenStore.delete(req.session.user.prefix);
|
|
||||||
}
|
|
||||||
req.session.user = userObj;
|
|
||||||
tokenStore.set(userObj.prefix, {
|
|
||||||
// Common
|
|
||||||
accessToken: userObj.accessToken,
|
|
||||||
defaultMethod: userObj.defaultMethod,
|
|
||||||
// WeChat test account
|
|
||||||
wechatAppId: userObj.wechatAppId,
|
|
||||||
wechatAppSecret: userObj.wechatAppSecret,
|
|
||||||
wechatTemplateId: userObj.wechatTemplateId,
|
|
||||||
wechatOpenId: userObj.wechatOpenId,
|
|
||||||
wechatVerifyToken: userObj.wechatVerifyToken,
|
|
||||||
wechatToken: await requestWeChatToken(
|
|
||||||
userObj.wechatAppId,
|
|
||||||
userObj.wechatAppSecret
|
|
||||||
),
|
|
||||||
// Email
|
|
||||||
email: userObj.email,
|
|
||||||
smtpServer: userObj.smtpServer,
|
|
||||||
smtpUser: userObj.smtpUser,
|
|
||||||
smtpPass: userObj.smtpPass,
|
|
||||||
// WeChat corporation account
|
|
||||||
corpId: userObj.corpId,
|
|
||||||
corpAgentId: userObj.corpAgentId,
|
|
||||||
corpAppSecret: userObj.corpAppSecret,
|
|
||||||
corpUserId: userObj.corpUserId,
|
|
||||||
corpToken: await requestCorpToken(userObj.corpId, userObj.corpAppSecret),
|
|
||||||
});
|
|
||||||
message = '配置更新成功';
|
|
||||||
console.debug(tokenStore);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
message = e.message;
|
|
||||||
}
|
|
||||||
req.flash('message', message);
|
|
||||||
res.redirect('/configure');
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
@@ -1,34 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const { Message } = require('../models');
|
|
||||||
const { md2html } = require('../common/utils');
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.get('/delete/:id', (req, res, next) => {
|
|
||||||
// TODO: delete message
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: 'Ok',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id', async (req, res, next) => {
|
|
||||||
const id = req.params.id;
|
|
||||||
try {
|
|
||||||
let message = await Message.findOne({
|
|
||||||
where: {
|
|
||||||
id: id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (message) {
|
|
||||||
message.content = md2html(message.content);
|
|
||||||
res.render('article', {
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
res.status(404);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
@@ -1,48 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
const Message = require('../models/message').Message;
|
|
||||||
const { processMessage } = require('../common/message');
|
|
||||||
const { tokenStore } = require('../common/token');
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.all('/:userPrefix/verify', (req, res, next) => {
|
|
||||||
// 验证消息来自微信服务器:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html
|
|
||||||
const userPrefix = req.params.userPrefix;
|
|
||||||
const { signature, timestamp, nonce, echostr } = req.query;
|
|
||||||
const token = tokenStore.get(userPrefix).wechatVerifyToken;
|
|
||||||
let tmp_array = [token, timestamp, nonce].sort();
|
|
||||||
let tmp_string = tmp_array.join('');
|
|
||||||
tmp_string = crypto.createHash('sha1').update(tmp_string).digest('hex');
|
|
||||||
if (tmp_string === signature) {
|
|
||||||
res.send(echostr);
|
|
||||||
} else {
|
|
||||||
res.send('verification failed');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.all('/:userPrefix/:description', async (req, res, next) => {
|
|
||||||
const userPrefix = req.params.userPrefix;
|
|
||||||
let message = {
|
|
||||||
title: '消息推送',
|
|
||||||
description: req.params.description,
|
|
||||||
token: req.query.token,
|
|
||||||
};
|
|
||||||
res.json(await processMessage(userPrefix, message));
|
|
||||||
});
|
|
||||||
|
|
||||||
router.all('/:userPrefix', async (req, res, next) => {
|
|
||||||
const userPrefix = req.params.userPrefix;
|
|
||||||
let message = {
|
|
||||||
type: req.query.type || req.body.type,
|
|
||||||
title: req.query.title || req.body.title || '无标题',
|
|
||||||
description: req.query.description || req.body.description || '无描述',
|
|
||||||
content: req.query.content || req.body.content,
|
|
||||||
email: req.query.email || req.body.email,
|
|
||||||
token: req.query.token || req.body.token,
|
|
||||||
};
|
|
||||||
let result = await processMessage(userPrefix, message);
|
|
||||||
res.json(result);
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
@@ -1,22 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8"/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
||||||
<title><%= message.title %></title>
|
|
||||||
<meta name="theme-color" content="#ffffff"/>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
|
||||||
<link rel="stylesheet" href="/public/main.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container" style="max-width: 960px">
|
|
||||||
<div style="margin: 16px 16px 16px 24px">
|
|
||||||
<h1 class="title"><%= message.title %></h1>
|
|
||||||
<h2 class="subtitle"><%- message.description %></h2>
|
|
||||||
<article id="article">
|
|
||||||
<%- message.content %>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@@ -1,279 +0,0 @@
|
|||||||
<%- include('./partials/header') %>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Credit: https://codepen.io/t7team/pen/ZowdRN
|
|
||||||
function openTab(e, tabName) {
|
|
||||||
let i, x, tabLinks;
|
|
||||||
x = document.getElementsByClassName('content-tab');
|
|
||||||
for (i = 0; i < x.length; i++) {
|
|
||||||
x[i].style.display = 'none';
|
|
||||||
}
|
|
||||||
tabLinks = document.getElementsByClassName('tab');
|
|
||||||
for (i = 0; i < x.length; i++) {
|
|
||||||
tabLinks[i].className = tabLinks[i].className.replace(' is-active', '');
|
|
||||||
}
|
|
||||||
document.getElementById(tabName).style.display = 'block';
|
|
||||||
e.className += ' is-active';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<div class="normal-container">
|
|
||||||
<div>
|
|
||||||
<h2 class="title">配置页面</h2>
|
|
||||||
<%- include('./partials/message') %>
|
|
||||||
<% if(showPasswordWarning) { %>
|
|
||||||
<article class="message is-warning">
|
|
||||||
<div class="message-header">
|
|
||||||
<p>警告</p>
|
|
||||||
</div>
|
|
||||||
<div class="message-body">
|
|
||||||
你正在使用的密码是默认密码,请尽快修改!
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<% } %>
|
|
||||||
<article class="message is-light">
|
|
||||||
<div class="message-header">
|
|
||||||
<p>注意</p>
|
|
||||||
</div>
|
|
||||||
<div class="message-body">
|
|
||||||
如果遇到问题,请仔细阅读
|
|
||||||
<a target="_blank"
|
|
||||||
href="https://github.com/songquanpeng/message-pusher/blob/master/README.md">README</a>,如果还不能解决,请提
|
|
||||||
<a target="_blank"
|
|
||||||
href="https://github.com/songquanpeng/message-pusher/issues/new">issue</a>。
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
如果本项目对你有意义,<a target="_blank" href="https://github.com/songquanpeng/message-pusher">请 ⭐ 该项目</a>,
|
|
||||||
<del>这是我继续维护的主要动力</del>
|
|
||||||
,谢谢 :)。
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<form action="/configure" method="post">
|
|
||||||
<div class="tabs">
|
|
||||||
<ul>
|
|
||||||
<li class="tab is-active" onclick="openTab(this,'userTab')"><a>用户设置</a></li>
|
|
||||||
<li class="tab" onclick="openTab(this,'testTab')"><a>微信测试号设置</a></li>
|
|
||||||
<li class="tab" onclick="openTab(this,'corpTab')"><a>微信企业号设置</a></li>
|
|
||||||
<li class="tab" onclick="openTab(this,'emailTab')"><a>邮件推送设置</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="userTab" class="content-tab">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">用户名</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="username" value="<%- username; %>" type="text"
|
|
||||||
placeholder="请输入新的用户名">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">密码</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="password" type="text" placeholder="请输入新的密码">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">默认推送方式</label>
|
|
||||||
<article class="message">
|
|
||||||
<div class="message-body">
|
|
||||||
可选值有:test(使用微信测试号进行推送),corp(使用微信企业号进行推送),email(使用邮件进行推送),client(使用客户端进行推送)
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="defaultMethod" value="<%- defaultMethod; %>" type="text"
|
|
||||||
placeholder="请输入新的默认推送方式">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">前缀</label>
|
|
||||||
<article class="message">
|
|
||||||
<div class="message-body">
|
|
||||||
前缀默认和用户名相同,如非必要,无需修改
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="prefix" value="<%- prefix ? prefix : username; %>" type="text"
|
|
||||||
placeholder="请输入你的前缀">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">ACCESS TOKEN</label>
|
|
||||||
<article class="message">
|
|
||||||
<div class="message-body">
|
|
||||||
Access Token 可防止未授权者利用本系统向你发送消息,留空则不做该检查
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="accessToken" value="<%- accessToken; %>" type="text"
|
|
||||||
placeholder="请设置新的 ACCESS TOKEN(可不填)">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="testTab" class="content-tab" style="display:none">
|
|
||||||
<article class="message">
|
|
||||||
<div class="message-body">
|
|
||||||
<a target="_blank"
|
|
||||||
href="https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index">微信公众平台测试号配置链接</a>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
根据你在 config.js 中填写的 href 以及你的 prefix,你的接口验证 URL为:<%- verifyUrl; %>。
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">APP ID</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="wechatAppId" value="<%- wechatAppId; %>" type="text"
|
|
||||||
placeholder="请输入 APP ID">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">APP SECRET</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="wechatAppSecret" value="<%- wechatAppSecret; %>" type="text"
|
|
||||||
placeholder="请输入 APP SECRET">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">OPEN ID</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="wechatOpenId" value="<%- wechatOpenId; %>" type="text"
|
|
||||||
placeholder="请输入你的 OPEN ID">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">TEMPLATE ID</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="wechatTemplateId" value="<%- wechatTemplateId; %>" type="text"
|
|
||||||
|
|
||||||
placeholder="请输入一个模板通知 ID">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">TOKEN</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="wechatVerifyToken" value="<%- wechatVerifyToken; %>" type="text"
|
|
||||||
|
|
||||||
placeholder="请输入你的 TOKEN">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="corpTab" class="content-tab" style="display:none">
|
|
||||||
<article class="message">
|
|
||||||
<div class="message-body">
|
|
||||||
<a target="_blank" href="https://work.weixin.qq.com/">在此处注册微信企业号</a>,之后<a target="_blank"
|
|
||||||
href="https://work.weixin.qq.com/wework_admin/frame#profile/wxPlugin">在此处扫码关注</a>,
|
|
||||||
再之后<a target="_blank"
|
|
||||||
href="https://work.weixin.qq.com/wework_admin/frame#apps">在此处创建一个应用</a>。
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">企业 ID</label>
|
|
||||||
<article class="message">
|
|
||||||
<div class="message-body">
|
|
||||||
<a target="_blank"
|
|
||||||
href="https://work.weixin.qq.com/wework_admin/frame#profile">在此处找到企业 ID</a>
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="corpId" value="<%- corpId; %>" type="text"
|
|
||||||
placeholder="请输入企业 ID">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">应用 AgentId</label>
|
|
||||||
<article class="message">
|
|
||||||
<div class="message-body">
|
|
||||||
应用详情页可找到
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="corpAgentId" value="<%- corpAgentId; %>" type="text"
|
|
||||||
placeholder="请输入应用的 AgentId">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">应用 Secret</label>
|
|
||||||
<article class="message">
|
|
||||||
<div class="message-body">
|
|
||||||
应用详情页可找到
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="corpAppSecret" value="<%- corpAppSecret; %>" type="text"
|
|
||||||
placeholder="请输入应用的 Secret">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">用户账号</label>
|
|
||||||
<article class="message">
|
|
||||||
<div class="message-body">
|
|
||||||
<a target="_blank"
|
|
||||||
href="https://work.weixin.qq.com/wework_admin/frame#contacts">在此处找到你的用户账号</a>
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="corpUserId" value="<%- corpUserId; %>" type="text"
|
|
||||||
placeholder="请输入你的 corpUserId">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="emailTab" class="content-tab" style="display:none">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">默认目标邮箱</label>
|
|
||||||
<article class="message">
|
|
||||||
<div class="message-body">
|
|
||||||
要用 QQ 邮箱或者 Foxmail 邮箱,否则将无法通过微信得到邮件消息提醒
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="email" value="<%- email; %>" type="email" placeholder="请输入你的邮箱(可不填)">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">SMTP 服务器地址</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="smtpServer" value="<%- smtpServer; %>" type="text"
|
|
||||||
placeholder="如 smtp.qq.com">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">SMTP 账号</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="smtpUser" value="<%- smtpUser; %>" type="email"
|
|
||||||
placeholder="请输入 SMTP 账号(是一个邮箱地址)">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">SMTP 凭据</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="smtpPass" type="text" placeholder="请输入新的 SMTP Token">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field is-grouped is-grouped-right" style="margin-top: 16px">
|
|
||||||
<div class="control">
|
|
||||||
<input type="submit" class="button is-light" value="提交">
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<input type="reset" class="button is-light" value="重置">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<%- include('./partials/footer') %>
|
|
||||||
|
|
@@ -1,10 +0,0 @@
|
|||||||
</div>
|
|
||||||
<footer class="footer" style="background-color: white">
|
|
||||||
<div class="content has-text-centered" >
|
|
||||||
<p>
|
|
||||||
<strong><a href="https://github.com/songquanpeng/wechat-message-push">WeChat Message Push</a></strong> by <a href="https://github.com/songquanpeng">JustSong</a>. The source code is licensed
|
|
||||||
<a href="http://opensource.org/licenses/mit-license.php">MIT</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
@@ -1,27 +0,0 @@
|
|||||||
<%- include('./partials/header') %>
|
|
||||||
<div class="normal-container">
|
|
||||||
<%- include('./partials/message') %>
|
|
||||||
<% if(showGuidance) { %>
|
|
||||||
<article class="message is-warning">
|
|
||||||
<div class="message-header">
|
|
||||||
<p>系统尚未完成配置</p>
|
|
||||||
</div>
|
|
||||||
<div class="message-body">
|
|
||||||
点击<a href="/configure">此处</a>访问配置页面,如需帮助,请点击上方帮助按钮。
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<% }%>
|
|
||||||
<article class="message is-light">
|
|
||||||
<div class="message-header">
|
|
||||||
<p>系统状况</p>
|
|
||||||
</div>
|
|
||||||
<div class="message-body">
|
|
||||||
运行模式:<%= process.env.MODE === '1' ? "Heroku 模式" : "普通模式"%>
|
|
||||||
<br>
|
|
||||||
内存占用:<%= (process.memoryUsage().rss / (1024 *1024)).toFixed(2) %> MB
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<%- include('./partials/footer') %>
|
|
@@ -1,10 +0,0 @@
|
|||||||
<%- include('./header') %>
|
|
||||||
<article class="message is-primary">
|
|
||||||
<div class="message-header">
|
|
||||||
<p>注意</p>
|
|
||||||
</div>
|
|
||||||
<div class="message-body">
|
|
||||||
<%= message %>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<%- include('./footer') %>
|
|
@@ -1,35 +0,0 @@
|
|||||||
<%- include('./partials/header') %>
|
|
||||||
|
|
||||||
<div class="narrow-container">
|
|
||||||
<div>
|
|
||||||
<h2 class="title">用户登录</h2>
|
|
||||||
<%- include('./partials/message') %>
|
|
||||||
<form action="/login" method="post">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">用户名</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="username" type="text" required placeholder="请输入用户名">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">密码</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="password" type="password" required placeholder="请输入你的密码">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field is-grouped is-grouped-right">
|
|
||||||
<div class="control">
|
|
||||||
<input type="submit" class="button is-light" value="提交">
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<input type="reset" class="button is-light" value="重置">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<%- include('./partials/footer') %>
|
|
||||||
|
|
@@ -1,3 +0,0 @@
|
|||||||
<%- include('./partials/header') %>
|
|
||||||
<%- include('./partials/message') %>
|
|
||||||
<%- include('./partials/footer') %>
|
|
@@ -1,12 +0,0 @@
|
|||||||
</div>
|
|
||||||
<footer class="footer" style="background-color: white">
|
|
||||||
<div class="content has-text-centered" >
|
|
||||||
<p>
|
|
||||||
<a href="https://github.com/songquanpeng/message-pusher">消息推送服务</a> 由 <a href="https://github.com/songquanpeng">JustSong</a> 构建,源代码遵循
|
|
||||||
<a href="http://opensource.org/licenses/mit-license.php">MIT</a> 协议
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@@ -1,16 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<title>消息推送服务</title>
|
|
||||||
<meta name="theme-color" content="#ffffff"/>
|
|
||||||
<link rel="stylesheet" href="/public/bulma.min.css">
|
|
||||||
<link rel="stylesheet" href="/public/main.css">
|
|
||||||
<script src="/public/main.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<%- include('./nav') %>
|
|
||||||
<div class="container">
|
|
@@ -1,12 +0,0 @@
|
|||||||
<% if(message && message.length) { %>
|
|
||||||
<article id='message' class="message <%= isErrorMessage ? 'is-danger' : 'is-info'%>">
|
|
||||||
<div class="message-body">
|
|
||||||
<%= message %>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
setTimeout(function (){
|
|
||||||
document.getElementById('message').style.display='none';
|
|
||||||
}, 5000)
|
|
||||||
</script>
|
|
||||||
</article>
|
|
||||||
<% }%>
|
|
@@ -1,54 +0,0 @@
|
|||||||
<nav class="navbar nav-shadow" role="navigation" aria-label="main navigation">
|
|
||||||
<div class="container">
|
|
||||||
<div class="navbar-brand">
|
|
||||||
<a class="navbar-item is-size-5" href="/" style="font-weight: bold">
|
|
||||||
消息推送服务
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false"
|
|
||||||
data-target="mainNavbar">
|
|
||||||
<span aria-hidden="true"></span>
|
|
||||||
<span aria-hidden="true"></span>
|
|
||||||
<span aria-hidden="true"></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div id="mainNavbar" class="navbar-menu">
|
|
||||||
<div class="navbar-start">
|
|
||||||
<a class="navbar-item" href="/"> 首页 </a>
|
|
||||||
<a class="navbar-item" target="_blank" href="https://github.com/songquanpeng/message-pusher/blob/master/README.md"> 帮助 </a>
|
|
||||||
<a class="navbar-item" target="_blank" href="https://iamazing.cn/page/message-pusher"> 关于 </a>
|
|
||||||
</div>
|
|
||||||
<div class="navbar-end">
|
|
||||||
<div class="navbar-item">
|
|
||||||
<div class="buttons">
|
|
||||||
<% if (isLogged) { %>
|
|
||||||
<% if (isAdmin) { %>
|
|
||||||
<a class="button is-light" href="/register">添加新用户</a>
|
|
||||||
<% } %>
|
|
||||||
<a class="button is-light" href="/configure">配置</a>
|
|
||||||
<a class="button is-light" href="/logout">退出</a>
|
|
||||||
<% } else { %>
|
|
||||||
<a class="button is-light" href="/register">注册</a>
|
|
||||||
<a class="button is-light" href="/login">登录</a>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
|
|
||||||
if ($navbarBurgers.length > 0) {
|
|
||||||
$navbarBurgers.forEach( el => {
|
|
||||||
el.addEventListener('click', () => {
|
|
||||||
const target = el.dataset.target;
|
|
||||||
const $target = document.getElementById(target);
|
|
||||||
el.classList.toggle('is-active');
|
|
||||||
$target.classList.toggle('is-active');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
@@ -1,12 +0,0 @@
|
|||||||
<h1 class="title is-3 is-4-mobile"><%-page.title%></h1>
|
|
||||||
<div class="info">
|
|
||||||
<span class="line">
|
|
||||||
Tag:
|
|
||||||
<% page.tag.trim().split(" ").forEach(function (tag) {if (tag !== "") {%>
|
|
||||||
<a class="tag is-light" href='/tag/<%= tag %>'><%= tag %></a>
|
|
||||||
<% }}); %>
|
|
||||||
</span>
|
|
||||||
<span class="line">Posted on <span class="tag is-light"><%-page.post_time%></span></span>
|
|
||||||
<span class="line">Edited on <span class="tag is-light"><%-page.edit_time%></span></span>
|
|
||||||
<span class="line">Views: <span class="tag is-light"><%-page.view%></span></span>
|
|
||||||
</div>
|
|
@@ -1,35 +0,0 @@
|
|||||||
<%- include('./partials/header') %>
|
|
||||||
|
|
||||||
<div class="narrow-container">
|
|
||||||
<div>
|
|
||||||
<h2 class="title">用户注册</h2>
|
|
||||||
<%- include('./partials/message') %>
|
|
||||||
<form action="/register" method="post">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">用户名</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="username" type="text" required placeholder="请输入用户名">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">密码</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="password" type="password" required placeholder="请输入你的密码">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field is-grouped is-grouped-right">
|
|
||||||
<div class="control">
|
|
||||||
<input type="submit" class="button is-light" value="提交">
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<input type="reset" class="button is-light" value="重置">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<%- include('./partials/footer') %>
|
|
||||||
|
|
26
web/.gitignore
vendored
Normal file
26
web/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.idea
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
21
web/README.md
Normal file
21
web/README.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# React Template
|
||||||
|
|
||||||
|
## Basic Usages
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Runs the app in the development mode
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Builds the app for production to the `build` folder
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to change the default server, please set `REACT_APP_SERVER` environment variables before build,
|
||||||
|
for example: `REACT_APP_SERVER=http://your.domain.com`.
|
||||||
|
|
||||||
|
Before you start editing, make sure your `Actions on Save` options have `Optimize imports` & `Run Prettier` enabled.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
1. https://github.com/OIerDb-ng/OIerDb
|
||||||
|
2. https://github.com/cornflourblue/react-hooks-redux-registration-login-example
|
49
web/package.json
Normal file
49
web/package.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "react-template",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.27.2",
|
||||||
|
"history": "^5.3.0",
|
||||||
|
"marked": "^4.1.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.3.0",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"react-toastify": "^9.0.8",
|
||||||
|
"semantic-ui-css": "^2.5.0",
|
||||||
|
"semantic-ui-react": "^2.1.3"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^2.7.1"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": true
|
||||||
|
},
|
||||||
|
"proxy": "http://localhost:3000"
|
||||||
|
}
|
BIN
web/public/favicon.ico
Normal file
BIN
web/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
18
web/public/index.html
Normal file
18
web/public/index.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="logo.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<title>消息推送服务</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
web/public/logo.png
Normal file
BIN
web/public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
3
web/public/robots.txt
Normal file
3
web/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
141
web/src/App.js
Normal file
141
web/src/App.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import React, { lazy, Suspense, useEffect } from 'react';
|
||||||
|
import { Route, Routes } from 'react-router-dom';
|
||||||
|
import Loading from './components/Loading';
|
||||||
|
import User from './pages/User';
|
||||||
|
import { PrivateRoute } from './components/PrivateRoute';
|
||||||
|
import RegisterForm from './components/RegisterForm';
|
||||||
|
import LoginForm from './components/LoginForm';
|
||||||
|
import NotFound from './pages/NotFound';
|
||||||
|
import Setting from './pages/Setting';
|
||||||
|
import EditUser from './pages/User/EditUser';
|
||||||
|
import AddUser from './pages/User/AddUser';
|
||||||
|
import { API, showError } from './helpers';
|
||||||
|
import PasswordResetForm from './components/PasswordResetForm';
|
||||||
|
import GitHubOAuth from './components/GitHubOAuth';
|
||||||
|
import PasswordResetConfirm from './components/PasswordResetConfirm';
|
||||||
|
|
||||||
|
const Home = lazy(() => import('./pages/Home'));
|
||||||
|
const About = lazy(() => import('./pages/About'));
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const loadStatus = async () => {
|
||||||
|
const res = await API.get('/api/status');
|
||||||
|
const { success, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
localStorage.setItem('status', JSON.stringify(data));
|
||||||
|
localStorage.setItem('footer_html', data.footer_html);
|
||||||
|
} else {
|
||||||
|
showError('无法正常连接至服务器!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadStatus().then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
|
<Home />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/user"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<User />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/user/edit/:id"
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
|
<EditUser />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/user/edit"
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
|
<EditUser />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/user/add"
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
|
<AddUser />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/user/reset"
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
|
<PasswordResetConfirm />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/login"
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
|
<LoginForm />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/register"
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
|
<RegisterForm />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/reset"
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
|
<PasswordResetForm />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/oauth/github"
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
|
<GitHubOAuth />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/setting"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
|
<Setting />
|
||||||
|
</Suspense>
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/about"
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
|
<About />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route path="*" element={NotFound} />
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
44
web/src/components/Footer.js
Normal file
44
web/src/components/Footer.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { Container, Segment } from 'semantic-ui-react';
|
||||||
|
|
||||||
|
const Footer = () => {
|
||||||
|
const [Footer, setFooter] = useState('');
|
||||||
|
useEffect(() => {
|
||||||
|
let savedFooter = localStorage.getItem('footer_html');
|
||||||
|
if (!savedFooter) savedFooter = '';
|
||||||
|
setFooter(savedFooter);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Segment vertical>
|
||||||
|
<Container textAlign="center">
|
||||||
|
{Footer === '' ? (
|
||||||
|
<div className="custom-footer">
|
||||||
|
<a
|
||||||
|
href="https://github.com/songquanpeng/message-pusher"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
消息推送服务 {process.env.REACT_APP_VERSION}{' '}
|
||||||
|
</a>
|
||||||
|
由{' '}
|
||||||
|
<a href="https://github.com/songquanpeng" target="_blank">
|
||||||
|
JustSong
|
||||||
|
</a>{' '}
|
||||||
|
构建,源代码遵循{' '}
|
||||||
|
<a href="https://opensource.org/licenses/mit-license.php">
|
||||||
|
MIT 协议
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="custom-footer"
|
||||||
|
dangerouslySetInnerHTML={{ __html: Footer }}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
</Segment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
57
web/src/components/GitHubOAuth.js
Normal file
57
web/src/components/GitHubOAuth.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { Dimmer, Loader, Segment } from 'semantic-ui-react';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import { API, showError, showSuccess } from '../helpers';
|
||||||
|
import { UserContext } from '../context/User';
|
||||||
|
|
||||||
|
const GitHubOAuth = () => {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const [userState, userDispatch] = useContext(UserContext);
|
||||||
|
const [prompt, setPrompt] = useState('处理中...');
|
||||||
|
const [processing, setProcessing] = useState(true);
|
||||||
|
|
||||||
|
let navigate = useNavigate();
|
||||||
|
|
||||||
|
const sendCode = async (code, count) => {
|
||||||
|
const res = await API.get(`/api/oauth/github?code=${code}`);
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
if (message === 'bind') {
|
||||||
|
showSuccess('绑定成功!');
|
||||||
|
navigate('/setting');
|
||||||
|
} else {
|
||||||
|
userDispatch({ type: 'login', payload: data });
|
||||||
|
localStorage.setItem('user', JSON.stringify(data));
|
||||||
|
showSuccess('登录成功!');
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
if (count === 0) {
|
||||||
|
setPrompt(`操作失败,重定向至登录界面中...`);
|
||||||
|
navigate('/setting'); // in case this is failed to bind GitHub
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
setPrompt(`出现错误,第 ${count} 次重试中...`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, count * 2000));
|
||||||
|
await sendCode(code, count);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let code = searchParams.get('code');
|
||||||
|
sendCode(code, 0).then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Segment style={{ minHeight: '300px' }}>
|
||||||
|
<Dimmer active inverted>
|
||||||
|
<Loader size='large'>{prompt}</Loader>
|
||||||
|
</Dimmer>
|
||||||
|
</Segment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GitHubOAuth;
|
183
web/src/components/Header.js
Normal file
183
web/src/components/Header.js
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import React, { useContext, useState } from 'react';
|
||||||
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
import { UserContext } from '../context/User';
|
||||||
|
|
||||||
|
import { Button, Container, Icon, Menu, Segment } from 'semantic-ui-react';
|
||||||
|
import { API, isAdmin, isMobile, showSuccess } from '../helpers';
|
||||||
|
import '../index.css';
|
||||||
|
|
||||||
|
// Header Buttons
|
||||||
|
const headerButtons = [
|
||||||
|
{
|
||||||
|
name: '首页',
|
||||||
|
to: '/',
|
||||||
|
icon: 'home',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '用户',
|
||||||
|
to: '/user',
|
||||||
|
icon: 'user',
|
||||||
|
admin: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '设置',
|
||||||
|
to: '/setting',
|
||||||
|
icon: 'setting',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '关于',
|
||||||
|
to: '/about',
|
||||||
|
icon: 'info circle',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
const [userState, userDispatch] = useContext(UserContext);
|
||||||
|
let navigate = useNavigate();
|
||||||
|
let size = isMobile() ? 'large' : '';
|
||||||
|
|
||||||
|
const [showSidebar, setShowSidebar] = useState(false);
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
setShowSidebar(false);
|
||||||
|
await API.get('/api/user/logout');
|
||||||
|
showSuccess('注销成功!');
|
||||||
|
userDispatch({ type: 'logout' });
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
navigate('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
setShowSidebar(!showSidebar);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderButtons = (isMobile) => {
|
||||||
|
return headerButtons.map((button) => {
|
||||||
|
if (button.admin && !isAdmin()) return <></>;
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
onClick={() => {
|
||||||
|
navigate(button.to);
|
||||||
|
setShowSidebar(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{button.name}
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Menu.Item key={button.name} as={Link} to={button.to}>
|
||||||
|
<Icon name={button.icon} />
|
||||||
|
{button.name}
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isMobile()) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Menu
|
||||||
|
borderless
|
||||||
|
size={size}
|
||||||
|
style={
|
||||||
|
showSidebar
|
||||||
|
? {
|
||||||
|
borderBottom: 'none',
|
||||||
|
marginBottom: '0',
|
||||||
|
borderTop: 'none',
|
||||||
|
height: '51px',
|
||||||
|
}
|
||||||
|
: { borderTop: 'none', height: '52px' }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Container>
|
||||||
|
<Menu.Item as={Link} to="/">
|
||||||
|
<img
|
||||||
|
src="/logo.png"
|
||||||
|
alt="logo"
|
||||||
|
style={{ marginRight: '0.75em' }}
|
||||||
|
/>
|
||||||
|
<div style={{ fontSize: '20px' }}>
|
||||||
|
<b>消息推送服务</b>
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Menu position="right">
|
||||||
|
<Menu.Item onClick={toggleSidebar}>
|
||||||
|
<Icon name={showSidebar ? 'close' : 'sidebar'} />
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Menu>
|
||||||
|
</Container>
|
||||||
|
</Menu>
|
||||||
|
{showSidebar ? (
|
||||||
|
<Segment style={{ marginTop: 0, borderTop: '0' }}>
|
||||||
|
<Menu secondary vertical style={{ width: '100%', margin: 0 }}>
|
||||||
|
{renderButtons(true)}
|
||||||
|
<Menu.Item>
|
||||||
|
{userState.user ? (
|
||||||
|
<Button onClick={logout}>注销</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setShowSidebar(false);
|
||||||
|
navigate('/login');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setShowSidebar(false);
|
||||||
|
navigate('/register');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
注册
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
</Segment>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Menu borderless size={size} style={{ borderTop: 'none' }}>
|
||||||
|
<Container>
|
||||||
|
<Menu.Item as={Link} to="/" className={'hide-on-mobile'}>
|
||||||
|
<img src="/logo.png" alt="logo" style={{ marginRight: '0.75em' }} />
|
||||||
|
<div style={{ fontSize: '20px' }}>
|
||||||
|
<b>消息推送服务</b>
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
{renderButtons(false)}
|
||||||
|
<Menu.Menu position="right">
|
||||||
|
{userState.user ? (
|
||||||
|
<Menu.Item
|
||||||
|
name="注销"
|
||||||
|
onClick={logout}
|
||||||
|
className="btn btn-link"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Menu.Item
|
||||||
|
name="登录"
|
||||||
|
as={Link}
|
||||||
|
to="/login"
|
||||||
|
className="btn btn-link"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Menu.Menu>
|
||||||
|
</Container>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
14
web/src/components/Loading.js
Normal file
14
web/src/components/Loading.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Segment, Dimmer, Loader } from 'semantic-ui-react';
|
||||||
|
|
||||||
|
const Loading = ({ prompt: name = 'page' }) => {
|
||||||
|
return (
|
||||||
|
<Segment style={{ height: 100 }}>
|
||||||
|
<Dimmer active inverted>
|
||||||
|
<Loader indeterminate>加载{name}中...</Loader>
|
||||||
|
</Dimmer>
|
||||||
|
</Segment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Loading;
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user