Compare commits

...

80 Commits

Author SHA1 Message Date
lejianwen
2948eaaa5c chore: Update Go version to 1.23 in build configurations 2025-06-16 15:41:16 +08:00
lejianwen
8641ba5c0c docs: Update swagger docs 2025-06-16 12:31:48 +08:00
lejianwen
60b7a18fe7 feat: Add PostgreSQL support and refactor MySQL DSN handling (#284) 2025-06-16 12:26:08 +08:00
lejianwen
ca068816ae feat: Add start time in /api/sysinfover 2025-06-16 12:23:48 +08:00
lejianwen
06648d9a6c fix(admin): Use admin-hello first
(#274) (#255)
2025-06-15 15:33:12 +08:00
puyujian
8a8abd5163 feat(oauth): 支持linux.do登录 (#280)
* 支持linux.do登录

* 修正
2025-06-15 15:32:20 +08:00
lejianwen
97f98cd6ce chore: update download links for musl cross-compilers 2025-06-05 12:14:17 +08:00
lejianwen
51f2920661 fix: Init sqlite fail(#266) 2025-06-04 09:31:43 +08:00
lejianwen
7a5d141ce8 fix(server): Port custom (#257) 2025-05-30 12:27:37 +08:00
lejianwen
3cef02a0bb fix(webclient): Peer online status 2025-05-29 18:51:37 +08:00
lejianwen
46a7ecc1ba fix: Captcha some problem when users login with same ip 2025-05-27 17:36:20 +08:00
lejianwen
4d2b037f5e docs: Readme 2025-05-25 17:44:29 +08:00
lejianwen
323364b24e feat(register): Register status can be set (#223) 2025-05-25 17:03:13 +08:00
lejianwen
f19109cdf8 feat(login): Captcha upgrade and add the function to ban IP addresses (#250) 2025-05-25 16:52:58 +08:00
Tao Chen
527260d60a fix: dn should be case-insensitive (#250) 2025-05-21 09:07:08 +08:00
lejianwen
46bb44f0ab fix(webclient): DefaultIdServerPort undefined (#238) 2025-05-16 20:14:36 +08:00
lejianwen
2f1380f24a fix(webclient): Remove license warning (#235) 2025-05-13 13:11:19 +08:00
lejianwen
ece3328e94 feat(webclient): Web client to 1.4.0 2025-05-12 20:16:08 +08:00
lejianwen
fdd26d87be fix: PageSize (#225) 2025-05-06 19:08:18 +08:00
lejianwen
2ade0dda42 chore: Noelware/docker-manifest-action 2025-04-25 16:20:36 +08:00
lejianwen
a87ae5cf65 chore: Noelware/docker-manifest-action 2025-04-25 14:34:45 +08:00
lejianwen
fe7b8b53a6 style: Oauth page languages 2025-04-24 21:52:43 +08:00
lejianwen
b929f3efdb style: Remove useless configurations 2025-04-15 10:52:46 +08:00
lejianwen
f847fc076f fix: Low case (#149) 2025-04-15 10:46:21 +08:00
lejianwen
60d0a701ce fix: Share pwd 2025-04-15 10:09:56 +08:00
lejianwen
0dedaf6824 feat: Peer share to group 2025-04-14 19:12:40 +08:00
lejianwen
ab231b3fed feat: Add SysInfoVer endpoint and AppService for version retrieval 2025-04-07 16:38:21 +08:00
lejianwen
e7f28cca36 fix: Update peer based on the UUID (#180) 2025-04-02 09:50:16 +08:00
lejianwen
505e8aac4b feat: Add Korean translations validator (#168) 2025-04-02 09:42:29 +08:00
lejianwen
746e2a6052 fix: Get Uuids 2025-03-15 21:02:47 +08:00
lejianwen
dc03d5d83d style: Update peer last online time logic (#173) 2025-03-15 21:02:08 +08:00
lejianwen
b770ab178d feat(admin): Add filter by ip and username (#172) 2025-03-15 19:49:49 +08:00
Tao Chen
fd7e022e88 fix: rm varify password accidentally (#176) 2025-03-15 19:40:02 +08:00
lejianwen
ac5df6826b fix: Init database err (#166) 2025-03-04 18:14:25 +08:00
lejianwen
91908859bc docs: Readme 2025-03-04 16:30:12 +08:00
lejianwen
9c8822f857 fix: templates 2025-03-04 16:14:34 +08:00
lejianwen
9709da7fb6 style(webclient): ws-host 2025-03-04 15:48:25 +08:00
lejianwen
1852f10131 feat(config): add ws-host configuration (#156) 2025-03-04 15:43:25 +08:00
Tao Chen
77d7b43e21 fix: Fix/ldap tls (#162)
* optimize and fix tls of LDAP

* fix
2025-03-02 22:59:01 +08:00
lejianwen
7410b539a0 style(service): refactor global dependencies to use local variables 2025-02-26 19:15:38 +08:00
lejianwen
0403d71502 feat(oauth): Oauth nonce (#148) 2025-02-26 16:36:53 +08:00
lejianwen
2af1d93a7d feat(oauth): Oauth callback page beautification (#115) 2025-02-25 13:51:49 +08:00
lejianwen
76281ad761 feat(api): Add device group for 1.3.8 2025-02-23 21:35:42 +08:00
lejianwen
d1ec9b4916 feat(api): Add /device-group/accessible for 1.3.8 2025-02-23 15:32:44 +08:00
lejianwen
448e7460a9 style(oidc): Oidc style 2025-02-21 09:49:41 +08:00
lejianwen
ee0cbabffc fix(admin): Admin hello 2025-02-20 19:37:34 +08:00
lejianwen
d6a5af890a style: No need exec sql on version 261 2025-02-20 19:22:22 +08:00
lejianwen
dc313441e5 fix: Js content-type 2025-02-19 16:04:03 +08:00
Tao Chen
c75320f4f4 feat(oidc): add pkce (#150) 2025-02-19 09:31:25 +08:00
lejianwen
c788f78416 docs: Readme 2025-02-17 10:59:38 +08:00
lejianwen
49cf954d4a fix(config)!: Token expire time (#145)
将配置中的过期时间单位统一为time.Duration,可以设置为`h`,`m`,`s`
2025-02-17 10:49:59 +08:00
lejianwen
014e3db54f docs: Readme 2025-02-16 21:06:55 +08:00
lejianwen
6d9c245c81 fix(webclient): port 2025-02-16 14:17:37 +08:00
lejianwen
7fa9b79f31 fix(webclient): port 2025-02-16 14:11:36 +08:00
lejianwen
c7f3d13b7f fix(webclient): port 2025-02-16 13:33:06 +08:00
lejianwen
46f08a89d2 feat: Login by pwd can be disable
---

Closes: #141
2025-02-16 13:06:45 +08:00
lejianwen
0dcfedb4dc fix(webclient)!: Webclient path is /ws/(relay|id) (#73 #143 #140)
Webclient 的反代引发了很多问题,现在将在HTTPS下的path固定为`/ws/(relay|id)`

---

Closes: #143 #140
2025-02-16 12:41:32 +08:00
lejianwen
918bf85a2d style: middleware name 2025-02-12 22:09:52 +08:00
lejianwen
99db5f7190 fix(admin): Admin web title 2025-02-12 21:10:07 +08:00
lejianwen
18eff791b2 style: Module name 2025-02-12 19:46:39 +08:00
lejianwen
624dcacac5 style: generate 2025-02-12 16:56:41 +08:00
lejianwen
878d5fd27c style: Remove generate 2025-02-12 16:25:04 +08:00
lejianwen
4b893ce0e8 docs: Docs 2025-02-12 16:14:51 +08:00
lejianwen
472524f836 style: Module name 2025-02-12 16:07:51 +08:00
lejianwen
dbf8b23b15 fix: Config watchConfig (#135)
---
Closes: #135
2025-02-10 10:13:34 +08:00
lejianwen
79a5dd53ae fix: User disabled can not work (#133)
---
Closes: #133
2025-02-10 10:13:15 +08:00
Tao Chen
8a5b20685c fix: When OIDC and LDAP work togethar (#132 #134)
* fix OIDC create user if LDAP enable

* `newUser.GroupId = 1` for ldap

* fix
2025-02-10 10:08:49 +08:00
lejianwen
5a9c972de0 docs: Readme 2025-02-09 21:13:01 +08:00
Tao Chen
fc0e67122d docs: add LDAP info (#130) 2025-02-09 19:36:31 +08:00
lejianwen
eb642f66ca docs: Readme 2025-02-07 18:14:12 +08:00
lejianwen
8cac15f7dd style: Log time 2025-02-07 17:57:28 +08:00
lejianwen
5011e2b7c1 feat: Web sso env (#125) 2025-02-07 17:52:40 +08:00
lejianwen
b0008143b1 docs: Up readme 2025-02-07 17:52:40 +08:00
lejianwen
a3c3ab5a72 style: Up conf 2025-02-07 17:52:40 +08:00
lejianwen
3a16269215 docs: Up readme 2025-02-07 17:52:40 +08:00
lejianwen
151145b0c3 feat: Random Initial Password for Admin (#117) 2025-02-07 17:52:39 +08:00
lejianwen
9c794e9d4b fix(build): Fix no admin in deb (#119 #120) 2025-02-03 13:29:35 +08:00
lejianwen
01f697d279 fix(api): Add Default Token Expire (#113) 2025-02-03 13:18:08 +08:00
lejianwen
6cdc37333b style: webclient 2025-02-03 00:01:10 +08:00
Tao Chen
ae32915565 feat(ldap): Add LDAP
* rename: Admin to AdminGroup

* update

* cleanup

* tmp save group mapping

* add enableControl(not-test)

* verify username exist before create(for LDAP)

* add getAllGroupsDn()

* rename

* adminGroup

* enable TLS Verify

* init for ldap

---------

Co-authored-by: Tao Chen <iamtaochen@outlook.com>
2025-02-02 23:59:52 +08:00
134 changed files with 96396 additions and 87298 deletions

View File

@@ -66,7 +66,7 @@ jobs:
- name: Set up Go environment
uses: actions/setup-go@v4
with:
go-version: '1.22' # 选择 Go 版本
go-version: '1.23' # 选择 Go 版本
- name: Set up npm
uses: actions/setup-node@v2
@@ -115,12 +115,12 @@ jobs:
zip -r ${{ matrix.job.goos}}-${{ matrix.job.platform }}.${{matrix.job.file_ext}} ./release
else
if [ "${{ matrix.job.platform }}" = "arm64" ]; then
wget https://musl.cc/aarch64-linux-musl-cross.tgz
wget https://musl.ljw.red/aarch64-linux-musl-cross.tgz
tar -xf aarch64-linux-musl-cross.tgz
export PATH=$PATH:$PWD/aarch64-linux-musl-cross/bin
GOOS=${{ matrix.job.goos }} GOARCH=${{ matrix.job.platform }} CC=aarch64-linux-musl-gcc CGO_LDFLAGS="-static" CGO_ENABLED=1 go build -ldflags "-s -w" -o ./release/apimain ./cmd/apimain.go
elif [ "${{ matrix.job.platform }}" = "armv7l" ]; then
wget https://musl.cc/armv7l-linux-musleabihf-cross.tgz
wget https://musl.ljw.red/armv7l-linux-musleabihf-cross.tgz
tar -xf armv7l-linux-musleabihf-cross.tgz
export PATH=$PATH:$PWD/armv7l-linux-musleabihf-cross/bin
GOOS=${{ matrix.job.goos }} GOARCH=arm GOARM=7 CC=armv7l-linux-musleabihf-gcc CGO_LDFLAGS="-static" CGO_ENABLED=1 go build -ldflags "-s -w" -o ./release/apimain ./cmd/apimain.go
@@ -147,6 +147,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate Changelog
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
@@ -203,6 +204,7 @@ jobs:
- name: Build package for ${{ matrix.job.platform }} arch
run: |
mv ${{ matrix.job.platform }}/release/apimain debian-build/${{ matrix.job.platform }}/bin/rustdesk-api
mv ${{ matrix.job.platform }}/release/resources/admin resources
chmod -v a+x debian-build/${{ matrix.job.platform }}/bin/*
mkdir -p data
cp -vr debian systemd conf data resources runtime debian-build/${{ matrix.job.platform }}/
@@ -379,7 +381,7 @@ jobs:
- name: Create and push manifest Docker Hub (:version)
if: ${{ env.SKIP_DOCKER_HUB == 'false' }}
uses: Noelware/docker-manifest-action@master
uses: Noelware/docker-manifest-action@v0.2.3
with:
base-image: ${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}
extra-images: ${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-amd64,
@@ -389,7 +391,7 @@ jobs:
- name: Create and push manifest GHCR (:version)
if: ${{ env.SKIP_GHCR == 'false' }}
uses: Noelware/docker-manifest-action@master
uses: Noelware/docker-manifest-action@v0.2.3
with:
base-image: ghcr.io/${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}
extra-images: ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-amd64,
@@ -400,7 +402,7 @@ jobs:
- name: Create and push manifest Docker Hub (:latest)
if: ${{ env.SKIP_DOCKER_HUB == 'false' }}
uses: Noelware/docker-manifest-action@master
uses: Noelware/docker-manifest-action@v0.2.3
with:
base-image: ${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:latest
extra-images: ${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:latest-amd64,
@@ -410,7 +412,7 @@ jobs:
- name: Create and push manifest GHCR (:latest)
if: ${{ env.SKIP_GHCR == 'false' }}
uses: Noelware/docker-manifest-action@master
uses: Noelware/docker-manifest-action@v0.2.3
with:
base-image: ghcr.io/${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:latest
extra-images: ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:latest-amd64,
@@ -421,7 +423,7 @@ jobs:
- name: Create and push Full S6 manifest Docker Hub (:version)
if: ${{ env.SKIP_DOCKER_HUB == 'false' }}
uses: Noelware/docker-manifest-action@master
uses: Noelware/docker-manifest-action@v0.2.3
with:
base-image: ${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:full-s6
extra-images: ${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:full-s6-amd64,
@@ -432,7 +434,7 @@ jobs:
- name: Create and push Full S6 manifest GHCR (:latest)
if: ${{ env.SKIP_GHCR == 'false' }}
uses: Noelware/docker-manifest-action@master
uses: Noelware/docker-manifest-action@v0.2.3
with:
base-image: ghcr.io/${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:full-s6
extra-images: ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:full-s6-amd64,

View File

@@ -61,7 +61,7 @@ jobs:
- name: Set up Go environment
uses: actions/setup-go@v4
with:
go-version: '1.22' # 选择 Go 版本
go-version: '1.23' # 选择 Go 版本
- name: Set up npm
uses: actions/setup-node@v2
@@ -101,12 +101,12 @@ jobs:
zip -r ${{ matrix.job.goos}}-${{ matrix.job.platform }}.${{matrix.job.file_ext}} ./release
else
if [ "${{ matrix.job.platform }}" = "arm64" ]; then
wget https://musl.cc/aarch64-linux-musl-cross.tgz
wget https://musl.ljw.red/aarch64-linux-musl-cross.tgz
tar -xf aarch64-linux-musl-cross.tgz
export PATH=$PATH:$PWD/aarch64-linux-musl-cross/bin
GOOS=${{ matrix.job.goos }} GOARCH=${{ matrix.job.platform }} CC=aarch64-linux-musl-gcc CGO_LDFLAGS="-static" CGO_ENABLED=1 go build -ldflags "-s -w" -o ./release/apimain ./cmd/apimain.go
elif [ "${{ matrix.job.platform }}" = "armv7l" ]; then
wget https://musl.cc/armv7l-linux-musleabihf-cross.tgz
wget https://musl.ljw.red/armv7l-linux-musleabihf-cross.tgz
tar -xf armv7l-linux-musleabihf-cross.tgz
export PATH=$PATH:$PWD/armv7l-linux-musleabihf-cross/bin
GOOS=${{ matrix.job.goos }} GOARCH=arm GOARM=7 CC=armv7l-linux-musleabihf-gcc CGO_LDFLAGS="-static" CGO_ENABLED=1 go build -ldflags "-s -w" -o ./release/apimain ./cmd/apimain.go
@@ -317,7 +317,7 @@ jobs:
- name: Create and push manifest Docker Hub (:version)
if: ${{ env.SKIP_DOCKER_HUB == 'false' }}
uses: Noelware/docker-manifest-action@master
uses: Noelware/docker-manifest-action@v0.2.3
with:
base-image: ${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}
extra-images: ${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-amd64,
@@ -327,7 +327,7 @@ jobs:
- name: Create and push manifest GHCR (:version)
if: ${{ env.SKIP_GHCR == 'false' }}
uses: Noelware/docker-manifest-action@master
uses: Noelware/docker-manifest-action@v0.2.3
with:
base-image: ghcr.io/${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}
extra-images: ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-amd64,

2
.gitignore vendored
View File

@@ -5,4 +5,4 @@ runtime/*
go.sum
resources/admin
release
data
data/rustdeskapi.db

154
README.md
View File

@@ -4,11 +4,12 @@
本项目使用 Go 实现了 RustDesk 的 API并包含了 Web Admin 和 Web 客户端。RustDesk 是一个远程桌面软件,提供了自托管的解决方案。
<div align=center>
<div align=center>
<img src="https://img.shields.io/badge/golang-1.22-blue"/>
<img src="https://img.shields.io/badge/gin-v1.9.0-lightBlue"/>
<img src="https://img.shields.io/badge/gorm-v1.25.7-green"/>
<img src="https://img.shields.io/badge/swag-v1.16.3-yellow"/>
<img src="https://goreportcard.com/badge/github.com/lejianwen/rustdesk-api/v2"/>
<img src="https://github.com/lejianwen/rustdesk-api/actions/workflows/build.yml/badge.svg"/>
</div>
@@ -19,7 +20,10 @@
- 登录
- 地址簿
- 群组
- 授权登录,支持`github`, `google``OIDC` 登录,支持`web后台`授权登录
- 授权登录
- 支持`github`, `google``OIDC` 登录,
- 支持`web后台`授权登录
- 支持`LDAP`(AD和OpenLDAP已测试), 如果API Server配置了LDAP
- i18n
- Web Admin
- 用户管理
@@ -28,6 +32,7 @@
- 标签管理
- 群组管理
- Oauth 管理
- 配置LDAP, 配置文件或者环境变量
- 登录日志
- 链接日志
- 文件传输日志
@@ -46,6 +51,7 @@
## 功能
### API 服务
基本实现了PC端基础的接口。支持Personal版本接口可以通过配置文件`rustdesk.personal`或环境变量`RUSTDESK_API_RUSTDESK_PERSONAL`来控制是否启用
@@ -70,23 +76,20 @@
* 使用前后端分离,提供用户友好的管理界面,主要用来管理和展示。前端代码在[rustdesk-api-web](https://github.com/lejianwen/rustdesk-api-web)
* 后台访问地址是`http://<your server>[:port]/_admin/`初次安装管理员为用户名密码为`admin` `admin`,请即时更改密码
* 后台访问地址是`http://<your server>[:port]/_admin/`
* 初次安装管理员为用户名为`admin`,密码将在控制台打印,可以通过[命令行](#CLI)更改密码
![img.png](./docs/init_admin_pwd.png)
1. 管理员界面
![web_admin](docs/web_admin.png)
2. 普通用户界面
![web_user](docs/web_admin_user.png)
右上角可以更改密码,可以切换语言,可以切换`白天/黑夜`模式
![web_resetpwd](docs/web_resetpwd.png)
3. 每个用户可以多个地址簿,也可以将地址簿共享给其他用户
4. 分组可以自定义,方便管理,暂时支持两种类型: `共享组``普通组`
5. 可以直接打开webclient方便使用也可以分享给游客游客可以直接通过webclient远程到设备
![web_webclient](docs/admin_webclient.png)
6. Oauth,支持了`Github`, `Google` 以及 `OIDC`, 需要创建一个`OAuth App`,然后配置到后台
![web_admin_oauth](docs/web_admin_oauth.png)
- 对于`Google``Github`, `Issuer``Scopes`不需要填写.
- 对于`OIDC`, `Issuer`是必须的。`Scopes`是可选的,默认为 `openid,profile,email`. 确保可以获取 `sub`,`email``preferred_username`
- `github oauth app``Settings`->`Developer settings`->`OAuth Apps`->`New OAuth App`
@@ -105,9 +108,9 @@
* 可以官方指令
* 可以添加自定义指令
* 可以执行自定义指令
![rustdesk_command_advance](./docs/rustdesk_command_advance.png)
11. **LDAP 支持**, 当在API Server上设置了LDAP(已测试AD和LDAP),可以通过LDAP中的用户信息进行登录 https://github.com/lejianwen/rustdesk-api/issues/114 ,如果LDAP验证失败返回本地用户
### Web Client:
@@ -127,6 +130,7 @@
![api_swag](docs/api_swag.png)
### CLI
```bash
# 查看帮助
./apimain -h
@@ -141,88 +145,57 @@
### 相关配置
* [配置文件](./conf/config.yaml)
* 参考`conf/config.yaml`配置文件,修改相关配置。
* 如果`gorm.type``sqlite`则不需要配置mysql相关配置。
* 语言如果不设置默认为`zh-CN`
```yaml
lang: "en"
app:
web-client: 1 # 1:启用 0:禁用
register: false #是否开启注册
show-swagger: 0 #是否显示swagger文档
gin:
api-addr: "0.0.0.0:21114"
mode: "release"
resources-path: 'resources'
trust-proxy: ""
gorm:
type: "sqlite"
max-idle-conns: 10
max-open-conns: 100
mysql:
username: "root"
password: "111111"
addr: "192.168.1.66:3308"
dbname: "rustdesk"
rustdesk:
id-server: "192.168.1.66:21116"
relay-server: "192.168.1.66:21117"
api-server: "http://192.168.1.66:21114"
key: "123456789"
personal: 1
logger:
path: "./runtime/log.txt"
level: "warn" #trace,debug,info,warn,error,fatal
report-caller: true
proxy:
enable: false
host: ""
jwt:
key: ""
expire-duration: 360000
```
### 环境变量
变量名前缀是`RUSTDESK_API`,环境变量如果存在将覆盖配置文件中的配置
环境变量和配置文件`conf/config.yaml`中的配置一一对应,变量名前缀是`RUSTDESK_API`
下面表格并未全部列出,可以参考`conf/config.yaml`中的配置。
| 变量名 | 说明 | 示例 |
|---------------------------------------------------|---------------------------------------------------------|------------------------------|
| TZ | 时区 | Asia/Shanghai |
| RUSTDESK_API_LANG | 语言 | `en`,`zh-CN` |
| RUSTDESK_API_APP_WEB_CLIENT | 是否启用web-client; 1:启用,0:不启用; 默认启用 | 1 |
| RUSTDESK_API_APP_REGISTER | 是否开启注册; `true`, `false` 默认`false` | `false` |
| RUSTDESK_API_APP_SHOW_SWAGGER | 是否可见swagger文档;`1`显示,`0`不显示,默认`0`不显示 | `1` |
| RUSTDESK_API_APP_TOKEN_EXPIRE | token有效时长(秒) | `3600` |
| -----ADMIN配置----- | ---------- | ---------- |
| RUSTDESK_API_ADMIN_TITLE | 后台标题 | `RustDesk Api Admin` |
| RUSTDESK_API_ADMIN_HELLO | 后台欢迎语,可以使用`html` | |
| RUSTDESK_API_ADMIN_HELLO_FILE | 后台欢迎语文件,如果内容多,使用文件更方便。<br>会覆盖`RUSTDESK_API_ADMIN_HELLO` | `./conf/admin/hello.html` |
| -----GIN配置----- | ---------- | ---------- |
| RUSTDESK_API_GIN_TRUST_PROXY | 信任的代理IP列表`,`分割,默认信任所有 | 192.168.1.2,192.168.1.3 |
| -----------GORM配置---------------- | ------------------------------------ | --------------------------- |
| RUSTDESK_API_GORM_TYPE | 数据库类型sqlite或者mysql默认sqlite | sqlite |
| RUSTDESK_API_GORM_MAX_IDLE_CONNS | 数据库最大空闲连接数 | 10 |
| RUSTDESK_API_GORM_MAX_OPEN_CONNS | 数据库最大打开连接数 | 100 |
| RUSTDESK_API_RUSTDESK_PERSONAL | 是否启用个人版API 1:启用,0:不启用; 默认启用 | 1 |
| -----MYSQL配置----- | ---------- | ---------- |
| RUSTDESK_API_MYSQL_USERNAME | mysql用户名 | root |
| RUSTDESK_API_MYSQL_PASSWORD | mysql密码 | 111111 |
| RUSTDESK_API_MYSQL_ADDR | mysql地址 | 192.168.1.66:3306 |
| RUSTDESK_API_MYSQL_DBNAME | mysql数据库名 | rustdesk |
| -----RUSTDESK配置----- | --------------- | ---------- |
| RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk的id服务器地址 | 192.168.1.66:21116 |
| RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk的relay服务器地址 | 192.168.1.66:21117 |
| RUSTDESK_API_RUSTDESK_API_SERVER | Rustdesk的api服务器地址 | http://192.168.1.66:21114 |
| RUSTDESK_API_RUSTDESK_KEY | Rustdesk的key | 123456789 |
| RUSTDESK_API_RUSTDESK_KEY_FILE | Rustdesk存放key的文件 | `./conf/data/id_ed25519.pub` |
| RUSTDESK_API_RUSTDESK_WEBCLIENT_MAGIC_QUERYONLINE | Web client v2 中是否启用新的在线状态查询方法; `1`:启用,`0`:不启用,默认不启用 | `0` |
| ----PROXY配置----- | --------------- | ---------- |
| RUSTDESK_API_PROXY_ENABLE | 是否启用代理:`false`, `true` | `false` |
| RUSTDESK_API_PROXY_HOST | 代理地址 | `http://127.0.0.1:1080` |
| ----JWT配置---- | -------- | -------- |
| RUSTDESK_API_JWT_KEY | 自定义JWT KEY,为空则不启用JWT | |
| RUSTDESK_API_JWT_EXPIRE_DURATION | JWT有效时间 | 360000 |
| 变量名 | 说明 | 示例 |
|--------------------------------------------------------|--------------------------------------------------------------------------------|------------------------------|
| TZ | 时区 | Asia/Shanghai |
| RUSTDESK_API_LANG | 语言 | `en`,`zh-CN` |
| RUSTDESK_API_APP_WEB_CLIENT | 是否启用web-client; 1:启用,0:不启用; 默认启用 | 1 |
| RUSTDESK_API_APP_REGISTER | 是否开启注册; `true`, `false` 默认`false` | `false` |
| RUSTDESK_API_APP_SHOW_SWAGGER | 是否可见swagger文档;`1`显示,`0`不显示,默认`0`不显示 | `1` |
| RUSTDESK_API_APP_TOKEN_EXPIRE | token有效时长 | `168h` |
| RUSTDESK_API_APP_DISABLE_PWD_LOGIN | 是否禁用密码登录; `true`, `false` 默认`false` | `false` |
| RUSTDESK_API_APP_REGISTER_STATUS | 注册用户默认状态; 1 启用2 禁用, 默认 1 | `1` |
| RUSTDESK_API_APP_CAPTCHA_THRESHOLD | 验证码触发次数; -1 不启用, 0 一直启用, >0 登录错误次数后启用 ;默认 `3` | `3` |
| RUSTDESK_API_APP_BAN_THRESHOLD | 封禁IP触发次数; 0 不启用, >0 登录错误次数后封禁IP; 默认 `0` | `0` |
| -----ADMIN配置----- | ---------- | ---------- |
| RUSTDESK_API_ADMIN_TITLE | 后台标题 | `RustDesk Api Admin` |
| RUSTDESK_API_ADMIN_HELLO | 后台欢迎语,可以使用`html` | |
| RUSTDESK_API_ADMIN_HELLO_FILE | 后台欢迎语文件,如果内容多,使用文件更方便。<br>会覆盖`RUSTDESK_API_ADMIN_HELLO` | `./conf/admin/hello.html` |
| -----GIN配置----- | ---------- | ---------- |
| RUSTDESK_API_GIN_TRUST_PROXY | 信任的代理IP列表`,`分割,默认信任所有 | 192.168.1.2,192.168.1.3 |
| -----GORM配置----- | ---------- | --------------------------- |
| RUSTDESK_API_GORM_TYPE | 数据库类型sqlite或者mysql默认sqlite | sqlite |
| RUSTDESK_API_GORM_MAX_IDLE_CONNS | 数据库最大空闲连接数 | 10 |
| RUSTDESK_API_GORM_MAX_OPEN_CONNS | 数据库最大打开连接数 | 100 |
| RUSTDESK_API_RUSTDESK_PERSONAL | 是否启用个人版API 1:启用,0:不启用; 默认启用 | 1 |
| -----MYSQL配置----- | ---------- | ---------- |
| RUSTDESK_API_MYSQL_USERNAME | mysql用户名 | root |
| RUSTDESK_API_MYSQL_PASSWORD | mysql密码 | 111111 |
| RUSTDESK_API_MYSQL_ADDR | mysql地址 | 192.168.1.66:3306 |
| RUSTDESK_API_MYSQL_DBNAME | mysql数据库名 | rustdesk |
| -----RUSTDESK配置----- | ---------- | ---------- |
| RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk的id服务器地址 | 192.168.1.66:21116 |
| RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk的relay服务器地址 | 192.168.1.66:21117 |
| RUSTDESK_API_RUSTDESK_API_SERVER | Rustdesk的api服务器地址 | http://192.168.1.66:21114 |
| RUSTDESK_API_RUSTDESK_KEY | Rustdesk的key | 123456789 |
| RUSTDESK_API_RUSTDESK_KEY_FILE | Rustdesk存放key的文件 | `./conf/data/id_ed25519.pub` |
| RUSTDESK_API_RUSTDESK_WEBCLIENT<br/>_MAGIC_QUERYONLINE | Web client v2 中是否启用新的在线状态查询方法; `1`:启用,`0`:不启用,默认不启用 | `0` |
| RUSTDESK_API_RUSTDESK_WS_HOST | 自定义Websocket Host | `wss://192.168.1.123:1234` |
| ----PROXY配置----- | ---------- | ---------- |
| RUSTDESK_API_PROXY_ENABLE | 是否启用代理:`false`, `true` | `false` |
| RUSTDESK_API_PROXY_HOST | 代理地址 | `http://127.0.0.1:1080` |
| ----JWT配置---- | -------- | -------- |
| RUSTDESK_API_JWT_KEY | 自定义JWT KEY,为空则不启用JWT<br/>如果没使用`lejianwen/rustdesk-server`中的`MUST_LOGIN`,建议设置为空 | |
| RUSTDESK_API_JWT_EXPIRE_DURATION | JWT有效时间 | `168h` |
### 运行
@@ -288,10 +261,11 @@ jwt:
6. 打开浏览器访问`http://<your server[:port]>/_admin/`,默认用户名密码为`admin`,请及时更改密码。
#### 使用我fork后的server-s6镜像运行
#### 使用`lejianwen/server-s6`镜像运行
- 已解决链接超时问题
- 可以强制登录后才能发起链接
- github https://github.com/lejianwen/rustdesk-server
- docker hub https://hub.docker.com/r/lejianwen/rustdesk-server-s6
```yaml
networks:
@@ -344,4 +318,4 @@ jwt:
<img src="https://contrib.rocks/image?repo=lejianwen/rustdesk-api" />
</a>
## 感谢你的支持!如果这个项目对你有帮助,请点个⭐️鼓励一下,谢谢!

View File

@@ -8,6 +8,7 @@ desktop software that provides self-hosted solutions.
<img src="https://img.shields.io/badge/gin-v1.9.0-lightBlue"/>
<img src="https://img.shields.io/badge/gorm-v1.25.7-green"/>
<img src="https://img.shields.io/badge/swag-v1.16.3-yellow"/>
<img src="https://goreportcard.com/badge/github.com/lejianwen/rustdesk-api/v2"/>
<img src="https://github.com/lejianwen/rustdesk-api/actions/workflows/build.yml/badge.svg"/>
</div>
@@ -18,7 +19,10 @@ desktop software that provides self-hosted solutions.
- Login
- Address Book
- Groups
- Authorized login, supports `GitHub`, `Google` and `OIDC` login, supports `web admin` authorized login
- Authorized login,
- supports `GitHub`, `Google` and `OIDC` login,
- supports `web admin` authorized login,
- supports LDAP(test AD and openladp) if API Server config
- i18n
- Web Admin
- User Management
@@ -27,6 +31,7 @@ desktop software that provides self-hosted solutions.
- Tag Management
- Group Management
- OAuth Management
- LDAP Config by config file or ENV
- Login Logs
- Connection Logs
- File Transfer Logs
@@ -69,23 +74,22 @@ Basic implementation of the PC client's primary interfaces.Supports the Personal
* The frontend and backend are separated to provide a user-friendly management interface, primarily for managing and
displaying data.Frontend code is available at [rustdesk-api-web](https://github.com/lejianwen/rustdesk-api-web)
* Admin panel URL: `http://<your server[:port]>/_admin/`. The default username and password for the initial
installation are `admin` `admin`, please change the password immediately.
* Admin panel URL: `http://<your server[:port]>/_admin/`
* For the initial installation, the admin username is `admin`, and the password will be printed in the console. You can change the password via the [command line](#CLI).
![img.png](./docs/init_admin_pwd.png)
1. Admin interface:
![web_admin](docs/en_img/web_admin.png)
2. Regular user interface:
![web_user](docs/en_img/web_admin_user.png)
In the top right corner, you can change the password, switch languages, and toggle between `day/night` mode.
![web_resetpwd](docs/en_img/web_resetpwd.png)
3. Each user can have multiple address books, which can also be shared with other users.
4. Groups can be customized for easy management. Currently, two types are supported: `shared group` and `regular group`.
5. You can directly launch the client or open the web client for convenience; you can also share it with guests, who can remotely access the device via the web client.
![web_webclient](docs/en_img/admin_webclient.png)
6. OAuth support: Currently, `GitHub`, `Google` and `OIDC` are supported. You need to create an `OAuth App` and configure it in
the admin panel.
![web_admin_oauth](docs/en_img/web_admin_oauth.png)
- For `Google` and `Github`, you don't need to fill the `Issuer` and `Scpoes`
- For `OIDC`, you must set the `Issuer`. And `Scopes` is optional which default is `openid,email,profile`, please make sure this `Oauth App` can access `sub`, `email` and `preferred_username`
- Create a `GitHub OAuth App`
@@ -97,19 +101,16 @@ installation are `admin` `admin`, please change the password immediately.
8. Connection logs
9. File transfer logs
10. Server control
- `Simple mode`, some simple commands have been GUI-ized and can be executed directly in the backend
![rustdesk_command_simple](./docs/en_img/rustdesk_command_simple.png)
- `Simple mode`, some simple commands have been GUI-ized and can be executed directly in the backend
![rustdesk_command_simple](./docs/en_img/rustdesk_command_simple.png)
- `Advanced mode`, commands can be executed directly in the backend
- `Advanced mode`, commands can be executed directly in the backend
* Official commands can be used
* Custom commands can be added
* Custom commands can be executed
![rustdesk_command_advance](./docs/en_img/rustdesk_command_advance.png)
11. **LDAP Support**, When you setup the LDAP(test for OpenLDAP and AD), you can login with the LDAP's user. https://github.com/lejianwen/rustdesk-api/issues/114 , if LDAP fail fallback local user
### Web Client:
1. If you're already logged into the admin panel, the web client will log in automatically.
@@ -142,88 +143,58 @@ installation are `admin` `admin`, please change the password immediately.
### Configuration
* [Config File](./conf/config.yaml)
* Modify the configuration in `conf/config.yaml`.
* If `gorm.type` is set to `sqlite`, MySQL-related configurations are not required.
* Language support: `en` and `zh-CN` are supported. The default is `zh-CN`.
```yaml
lang: "en"
app:
web-client: 1 # web client route 1:open 0:close
register: false #register enable
show-swagger: 0 #show swagger 1:open 0:close
gin:
api-addr: "0.0.0.0:21114"
mode: "release"
resources-path: 'resources'
trust-proxy: ""
gorm:
type: "sqlite"
max-idle-conns: 10
max-open-conns: 100
mysql:
username: "root"
password: "111111"
addr: "192.168.1.66:3308"
dbname: "rustdesk"
rustdesk:
id-server: "192.168.1.66:21116"
relay-server: "192.168.1.66:21117"
api-server: "http://192.168.1.66:21114"
key: "123456789"
personal: 1
logger:
path: "./runtime/log.txt"
level: "warn" #trace,debug,info,warn,error,fatal
report-caller: true
proxy:
enable: false
host: ""
jwt:
key: ""
expire-duration: 360000
```
### Environment Variables
The prefix for variable names is `RUSTDESK_API`. If environment variables exist, they will override the configurations in the configuration file.
The environment variables correspond one-to-one with the configurations in the `conf/config.yaml` file. The prefix for variable names is `RUSTDESK_API`.
The table below does not list all configurations. Please refer to the configurations in `conf/config.yaml`.
| Variable Name | Description | Example |
|---------------------------------------------------|--------------------------------------------------------------------------------------------------------------|-------------------------------|
| TZ | timezone | Asia/Shanghai |
| RUSTDESK_API_LANG | Language | `en`,`zh-CN` |
| RUSTDESK_API_APP_WEB_CLIENT | web client on/off; 1: on, 0 off, default: 1 | 1 |
| RUSTDESK_API_APP_REGISTER | register enable; `true`, `false`; default:`false` | `false` |
| RUSTDESK_API_APP_SHOW_SWAGGER | swagger visible; 1: yes, 0: no; default: 0 | `0` |
| RUSTDESK_API_APP_TOKEN_EXPIRE | token expire duration(second) | `3600` |
| ----- ADMIN Configuration----- | ---------- | ---------- |
| RUSTDESK_API_ADMIN_TITLE | Admin Title | `RustDesk Api Admin` |
| RUSTDESK_API_ADMIN_HELLO | Admin welcome message, you can use `html` | |
| RUSTDESK_API_ADMIN_HELLO_FILE | Admin welcome message file,<br>will override `RUSTDESK_API_ADMIN_HELLO` | `./conf/admin/hello.html` |
| ----- GIN Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_GIN_TRUST_PROXY | Trusted proxy IPs, separated by commas. | 192.168.1.2,192.168.1.3 |
| ----- GORM Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_GORM_TYPE | Database type (`sqlite` or `mysql`). Default is `sqlite`. | sqlite |
| RUSTDESK_API_GORM_MAX_IDLE_CONNS | Maximum idle connections | 10 |
| RUSTDESK_API_GORM_MAX_OPEN_CONNS | Maximum open connections | 100 |
| RUSTDESK_API_RUSTDESK_PERSONAL | Open Personal Api 1:Enable,0:Disable | 1 |
| ----- MYSQL Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_MYSQL_USERNAME | MySQL username | root |
| RUSTDESK_API_MYSQL_PASSWORD | MySQL password | 111111 |
| RUSTDESK_API_MYSQL_ADDR | MySQL address | 192.168.1.66:3306 |
| RUSTDESK_API_MYSQL_DBNAME | MySQL database name | rustdesk |
| ----- RUSTDESK Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk ID server address | 192.168.1.66:21116 |
| RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk relay server address | 192.168.1.66:21117 |
| RUSTDESK_API_RUSTDESK_API_SERVER | Rustdesk API server address | http://192.168.1.66:21114 |
| RUSTDESK_API_RUSTDESK_KEY | Rustdesk key | 123456789 |
| RUSTDESK_API_RUSTDESK_KEY_FILE | Rustdesk key file | `./conf/data/id_ed25519.pub` |
| RUSTDESK_API_RUSTDESK_WEBCLIENT_MAGIC_QUERYONLINE | New online query method is enabled in the web client v2; '1': Enabled, '0': Disabled, not enabled by default | `0` |
| ---- PROXY ----- | --------------- | ---------- |
| RUSTDESK_API_PROXY_ENABLE | proxy_enable :`false`, `true` | `false` |
| RUSTDESK_API_PROXY_HOST | proxy_host | `http://127.0.0.1:1080` |
| ----JWT---- | -------- | -------- |
| RUSTDESK_API_JWT_KEY | JWT KEY. Set empty to disable jwt | |
| RUSTDESK_API_JWT_EXPIRE_DURATION | JWT expire duration | 360000 |
| Variable Name | Description | Example |
|--------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------|
| TZ | timezone | Asia/Shanghai |
| RUSTDESK_API_LANG | Language | `en`,`zh-CN` |
| RUSTDESK_API_APP_WEB_CLIENT | web client on/off; 1: on, 0 off, default: 1 | 1 |
| RUSTDESK_API_APP_REGISTER | register enable; `true`, `false`; default:`false` | `false` |
| RUSTDESK_API_APP_SHOW_SWAGGER | swagger visible; 1: yes, 0: no; default: 0 | `0` |
| RUSTDESK_API_APP_TOKEN_EXPIRE | token expire duration | `168h` |
| RUSTDESK_API_APP_DISABLE_PWD_LOGIN | disable password login | `false` |
| RUSTDESK_API_APP_REGISTER_STATUS | register user default status ; 1 enabled , 2 disabled ; default 1 | `1` |
| RUSTDESK_API_APP_CAPTCHA_THRESHOLD | captcha threshold; -1 disabled, 0 always enable, >0 threshold ;default `3` | `3` |
| RUSTDESK_API_APP_BAN_THRESHOLD | ban ip threshold; 0 disabled, >0 threshold ; default `0` | `0` |
| ----- ADMIN Configuration----- | ---------- | ---------- |
| RUSTDESK_API_ADMIN_TITLE | Admin Title | `RustDesk Api Admin` |
| RUSTDESK_API_ADMIN_HELLO | Admin welcome message, you can use `html` | |
| RUSTDESK_API_ADMIN_HELLO_FILE | Admin welcome message file,<br>will override `RUSTDESK_API_ADMIN_HELLO` | `./conf/admin/hello.html` |
| ----- GIN Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_GIN_TRUST_PROXY | Trusted proxy IPs, separated by commas. | 192.168.1.2,192.168.1.3 |
| ----- GORM Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_GORM_TYPE | Database type (`sqlite` or `mysql`). Default is `sqlite`. | sqlite |
| RUSTDESK_API_GORM_MAX_IDLE_CONNS | Maximum idle connections | 10 |
| RUSTDESK_API_GORM_MAX_OPEN_CONNS | Maximum open connections | 100 |
| RUSTDESK_API_RUSTDESK_PERSONAL | Open Personal Api 1:Enable,0:Disable | 1 |
| ----- MYSQL Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_MYSQL_USERNAME | MySQL username | root |
| RUSTDESK_API_MYSQL_PASSWORD | MySQL password | 111111 |
| RUSTDESK_API_MYSQL_ADDR | MySQL address | 192.168.1.66:3306 |
| RUSTDESK_API_MYSQL_DBNAME | MySQL database name | rustdesk |
| ----- RUSTDESK Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk ID server address | 192.168.1.66:21116 |
| RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk relay server address | 192.168.1.66:21117 |
| RUSTDESK_API_RUSTDESK_API_SERVER | Rustdesk API server address | http://192.168.1.66:21114 |
| RUSTDESK_API_RUSTDESK_KEY | Rustdesk key | 123456789 |
| RUSTDESK_API_RUSTDESK_KEY_FILE | Rustdesk key file | `./conf/data/id_ed25519.pub` |
| RUSTDESK_API_RUSTDESK<br/>_WEBCLIENT_MAGIC_QUERYONLINE | New online query method is enabled in the web client v2; '1': Enabled, '0': Disabled, not enabled by default | `0` |
| RUSTDESK_API_RUSTDESK_WS_HOST | Custom Websocket Host | `wss://192.168.1.123:1234` |
| ---- PROXY ----- | --------------- | ---------- |
| RUSTDESK_API_PROXY_ENABLE | proxy_enable :`false`, `true` | `false` |
| RUSTDESK_API_PROXY_HOST | proxy_host | `http://127.0.0.1:1080` |
| ----JWT---- | -------- | -------- |
| RUSTDESK_API_JWT_KEY | Custom JWT KEY, if empty JWT is not enabled.<br/>If `MUST_LOGIN` from `lejianwen/rustdesk-server` is not used, it is recommended to leave it empty. | |
| RUSTDESK_API_JWT_EXPIRE_DURATION | JWT expire duration | `168h` |
### Installation Steps
@@ -294,8 +265,9 @@ Download the release from [release](https://github.com/lejianwen/rustdesk-api/re
#### Running with my forked server-s6 image
- Connection timeout issue resolved
- Can enforce login before initiating a connection
- github https://github.com/lejianwen/rustdesk-server
- docker hub https://hub.docker.com/r/lejianwen/rustdesk-server-s6
```yaml
networks:
@@ -344,3 +316,5 @@ Thanks to everyone who contributed!
<a href="https://github.com/lejianwen/rustdesk-api/graphs/contributors">
<img src="https://contrib.rocks/image?repo=lejianwen/rustdesk-api" />
</a>
## Thanks for your support! If you find this project useful, please give it a ⭐️. Thank you!

View File

@@ -1,19 +1,20 @@
package main
import (
"Gwen/config"
"Gwen/global"
"Gwen/http"
"Gwen/lib/cache"
"Gwen/lib/jwt"
"Gwen/lib/lock"
"Gwen/lib/logger"
"Gwen/lib/orm"
"Gwen/lib/upload"
"Gwen/model"
"Gwen/service"
"fmt"
"github.com/go-redis/redis/v8"
"github.com/lejianwen/rustdesk-api/v2/config"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http"
"github.com/lejianwen/rustdesk-api/v2/lib/cache"
"github.com/lejianwen/rustdesk-api/v2/lib/jwt"
"github.com/lejianwen/rustdesk-api/v2/lib/lock"
"github.com/lejianwen/rustdesk-api/v2/lib/logger"
"github.com/lejianwen/rustdesk-api/v2/lib/orm"
"github.com/lejianwen/rustdesk-api/v2/lib/upload"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service"
"github.com/lejianwen/rustdesk-api/v2/utils"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/spf13/cobra"
"os"
@@ -52,12 +53,16 @@ var resetPwdCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
pwd := args[0]
admin := service.AllService.UserService.InfoById(1)
err := service.AllService.UserService.UpdatePassword(admin, pwd)
if err != nil {
fmt.Printf("reset password fail! %v \n", err)
if admin.Id == 0 {
global.Logger.Warn("user not found! ")
return
}
fmt.Printf("reset password success! \n")
err := service.AllService.UserService.UpdatePassword(admin, pwd)
if err != nil {
global.Logger.Error("reset password fail! ", err)
return
}
global.Logger.Info("reset password success! ")
},
}
var resetUserPwdCmd = &cobra.Command{
@@ -70,20 +75,24 @@ var resetUserPwdCmd = &cobra.Command{
pwd := args[1]
uid, err := strconv.Atoi(userId)
if err != nil {
fmt.Printf("userId must be int! \n")
global.Logger.Warn("userId must be int!")
return
}
if uid <= 0 {
fmt.Printf("userId must be greater than 0! \n")
global.Logger.Warn("userId must be greater than 0! ")
return
}
u := service.AllService.UserService.InfoById(uint(uid))
err = service.AllService.UserService.UpdatePassword(u, pwd)
if err != nil {
fmt.Printf("reset password fail! %v \n", err)
if u.Id == 0 {
global.Logger.Warn("user not found! ")
return
}
fmt.Printf("reset password success! \n")
err = service.AllService.UserService.UpdatePassword(u, pwd)
if err != nil {
global.Logger.Warn("reset password fail! ", err)
return
}
global.Logger.Info("reset password success!")
},
}
@@ -93,7 +102,7 @@ func init() {
}
func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
global.Logger.Error(err)
os.Exit(1)
}
}
@@ -132,20 +141,41 @@ func InitGlobal() {
}
//gorm
if global.Config.Gorm.Type == config.TypeMysql {
dns := global.Config.Mysql.Username + ":" + global.Config.Mysql.Password + "@(" + global.Config.Mysql.Addr + ")/" + global.Config.Mysql.Dbname + "?charset=utf8mb4&parseTime=True&loc=Local"
dsn := fmt.Sprintf("%s:%s@(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
global.Config.Mysql.Username,
global.Config.Mysql.Password,
global.Config.Mysql.Addr,
global.Config.Mysql.Dbname,
)
global.DB = orm.NewMysql(&orm.MysqlConfig{
Dns: dns,
Dsn: dsn,
MaxIdleConns: global.Config.Gorm.MaxIdleConns,
MaxOpenConns: global.Config.Gorm.MaxOpenConns,
})
}, global.Logger)
} else if global.Config.Gorm.Type == config.TypePostgresql {
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s TimeZone=%s",
global.Config.Postgresql.Host,
global.Config.Postgresql.Port,
global.Config.Postgresql.User,
global.Config.Postgresql.Password,
global.Config.Postgresql.Dbname,
global.Config.Postgresql.Sslmode,
global.Config.Postgresql.TimeZone,
)
global.DB = orm.NewPostgresql(&orm.PostgresqlConfig{
Dsn: dsn,
MaxIdleConns: global.Config.Gorm.MaxIdleConns,
MaxOpenConns: global.Config.Gorm.MaxOpenConns,
}, global.Logger)
} else {
//sqlite
global.DB = orm.NewSqlite(&orm.SqliteConfig{
MaxIdleConns: global.Config.Gorm.MaxIdleConns,
MaxOpenConns: global.Config.Gorm.MaxOpenConns,
})
}, global.Logger)
}
DatabaseAutoUpdate()
//validator
global.ApiInitValidator()
@@ -162,43 +192,60 @@ func InitGlobal() {
//jwt
//fmt.Println(global.Config.Jwt.PrivateKey)
global.Jwt = jwt.NewJwt(global.Config.Jwt.Key, global.Config.Jwt.ExpireDuration*time.Second)
global.Jwt = jwt.NewJwt(global.Config.Jwt.Key, global.Config.Jwt.ExpireDuration)
//locker
global.Lock = lock.NewLocal()
//service
service.New(&global.Config, global.DB, global.Logger, global.Jwt, global.Lock)
global.LoginLimiter = utils.NewLoginLimiter(utils.SecurityPolicy{
CaptchaThreshold: global.Config.App.CaptchaThreshold,
BanThreshold: global.Config.App.BanThreshold,
AttemptsWindow: 10 * time.Minute,
BanDuration: 30 * time.Minute,
})
global.LoginLimiter.RegisterProvider(utils.B64StringCaptchaProvider{})
DatabaseAutoUpdate()
}
func DatabaseAutoUpdate() {
version := 260
version := 262
db := global.DB
if global.Config.Gorm.Type == config.TypeMysql {
//检查存不存在数据库,不存在则创建
dbName := db.Migrator().CurrentDatabase()
fmt.Println("dbName", dbName)
if dbName == "" {
dbName = global.Config.Mysql.Dbname
// 移除 DSN 中的数据库名称,以便初始连接时不指定数据库
dsnWithoutDB := global.Config.Mysql.Username + ":" + global.Config.Mysql.Password + "@(" + global.Config.Mysql.Addr + ")/?charset=utf8mb4&parseTime=True&loc=Local"
dsnWithoutDB := fmt.Sprintf("%s:%s@(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
global.Config.Mysql.Username,
global.Config.Mysql.Password,
global.Config.Mysql.Addr,
"",
)
//新链接
dbWithoutDB := orm.NewMysql(&orm.MysqlConfig{
Dns: dsnWithoutDB,
})
Dsn: dsnWithoutDB,
}, global.Logger)
// 获取底层的 *sql.DB 对象,并确保在程序退出时关闭连接
sqlDBWithoutDB, err := dbWithoutDB.DB()
if err != nil {
fmt.Printf("获取底层 *sql.DB 对象失败: %v\n", err)
global.Logger.Errorf("获取底层 *sql.DB 对象失败: %v", err)
return
}
defer func() {
if err := sqlDBWithoutDB.Close(); err != nil {
fmt.Printf("关闭连接失败: %v\n", err)
global.Logger.Errorf("关闭连接失败: %v", err)
}
}()
err = dbWithoutDB.Exec("CREATE DATABASE IF NOT EXISTS " + dbName + " DEFAULT CHARSET utf8mb4").Error
if err != nil {
fmt.Println(err)
global.Logger.Error(err)
return
}
}
@@ -213,6 +260,7 @@ func DatabaseAutoUpdate() {
if v.Version < uint(version) {
Migrate(uint(version))
}
// 245迁移
if v.Version < 245 {
//oauths 表的 oauth_type 字段设置为 op同样的值
@@ -235,7 +283,7 @@ func DatabaseAutoUpdate() {
}
func Migrate(version uint) {
fmt.Println("migrating....", version)
global.Logger.Info("Migrating....", version)
err := global.DB.AutoMigrate(
&model.Version{},
&model.User{},
@@ -253,9 +301,10 @@ func Migrate(version uint) {
&model.AddressBookCollection{},
&model.AddressBookCollectionRule{},
&model.ServerCmd{},
&model.DeviceGroup{},
)
if err != nil {
fmt.Println("migrate err :=>", err)
global.Logger.Error("migrate err :=>", err)
}
global.DB.Create(&model.Version{Version: version})
//如果是初次则创建一个默认用户
@@ -289,7 +338,11 @@ func Migrate(version uint) {
IsAdmin: &is_admin,
GroupId: 1,
}
admin.Password = service.AllService.UserService.EncryptPassword("admin")
// 生成随机密码
pwd := utils.RandomString(8)
global.Logger.Info("Admin Password Is: ", pwd)
admin.Password = service.AllService.UserService.EncryptPassword(pwd)
global.DB.Create(admin)
}

View File

@@ -1 +1 @@
### 👏👏👏 你好 ***{{username}}*** 欢迎使用 [RustDesk Api](https://github.com/lejianwen/rustdesk-api)
### 👏👏👏 你好 ***{{username}}*** 欢迎使用 [RustDesk API](https://github.com/lejianwen/rustdesk-api)

View File

@@ -2,12 +2,21 @@ lang: "zh-CN"
app:
web-client: 1 # 1:启用 0:禁用
register: false #是否开启注册
register-status: 1 # 注册用户默认状态 1:启用 2:禁用
captcha-threshold: 3 # <0:disabled, 0 always, >0:enabled
ban-threshold: 0 # 0:disabled, >0:enabled
show-swagger: 0 # 1:启用 0:禁用
token-expire: 360000
token-expire: 168h
web-sso: true #web auth sso
disable-pwd-login: false #禁用密码登录
admin:
title: "RustDesk Api Admin"
title: "RustDesk API Admin"
hello-file: "./conf/admin/hello.html" #优先使用file
hello: ""
# ID Server and Relay Server ports https://github.com/lejianwen/rustdesk-api/issues/257
id-server-port: 21116 # ID Server port (for server cmd)
relay-server-port: 21117 # ID Server port (for server cmd)
gin:
api-addr: "0.0.0.0:21114"
mode: "release" #release,debug,test
@@ -22,6 +31,16 @@ mysql:
password: ""
addr: ""
dbname: ""
postgresql:
host: "127.0.0.1"
port: "5432"
user: ""
password: ""
dbname: "postgres"
sslmode: "disable" # disable, require, verify-ca, verify-full
time-zone: "Asia/Shanghai" # Time zone for PostgreSQL connection
rustdesk:
id-server: "192.168.1.66:21116"
relay-server: "192.168.1.66:21117"
@@ -30,6 +49,7 @@ rustdesk:
key-file: "/data/id_ed25519.pub"
personal: 1
webclient-magic-queryonline: 0
ws-host: "" #eg: wss://192.168.1.3:4443
logger:
path: "./runtime/log.txt"
level: "info" #trace,debug,info,warn,error,fatal
@@ -39,22 +59,25 @@ proxy:
host: "http://127.0.0.1:1080"
jwt:
key: ""
expire-duration: 360000
redis:
addr: "127.0.0.1:6379"
password: ""
db: 0
cache:
type: "file"
file-dir: "./runtime/cache"
redis-addr: "127.0.0.1:6379"
redis-pwd: ""
redis-db: 0
oss:
access-key-id: ""
access-key-secret: ""
host: ""
callback-url: ""
expire-time: 30
max-byte: 10240
expire-duration: 168h
ldap:
enable: false
url: "ldap://ldap.example.com:389"
tls-ca-file: ""
tls-verify: false
base-dn: "dc=example,dc=com"
bind-dn: "cn=admin,dc=example,dc=com"
bind-password: "password"
user:
base-dn: "ou=users,dc=example,dc=com"
enable-attr: "" #The attribute name of the user for enabling, in AD it is "userAccountControl", empty means no enable attribute, all users are enabled
enable-attr-value: "" # The value of the enable attribute when the user is enabled. If you are using AD, just set random value, it will be ignored.
filter: "(cn=*)"
username: "uid" # The attribute name of the user for usernamem if you are using AD, it should be "sAMAccountName"
email: "mail"
first-name: "givenName"
last-name: "sn"
sync: false # If true, the user will be synchronized to the database when the user logs in. If false, the user will be synchronized to the database when the user be created.
admin-group: "cn=admin,dc=example,dc=com" # The group name of the admin group, if the user is in this group, the user will be an admin.

View File

@@ -2,9 +2,9 @@ package config
import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"strings"
"time"
)
const (
@@ -14,30 +14,48 @@ const (
)
type App struct {
WebClient int `mapstructure:"web-client"`
Register bool `mapstructure:"register"`
ShowSwagger int `mapstructure:"show-swagger"`
TokenExpire int `mapstructure:"token-expire"`
WebClient int `mapstructure:"web-client"`
Register bool `mapstructure:"register"`
RegisterStatus int `mapstructure:"register-status"`
ShowSwagger int `mapstructure:"show-swagger"`
TokenExpire time.Duration `mapstructure:"token-expire"`
WebSso bool `mapstructure:"web-sso"`
DisablePwdLogin bool `mapstructure:"disable-pwd-login"`
CaptchaThreshold int `mapstructure:"captcha-threshold"`
BanThreshold int `mapstructure:"ban-threshold"`
}
type Admin struct {
Title string `mapstructure:"title"`
Hello string `mapstructure:"hello"`
HelloFile string `mapstructure:"hello-file"`
Title string `mapstructure:"title"`
Hello string `mapstructure:"hello"`
HelloFile string `mapstructure:"hello-file"`
IdServerPort int `mapstructure:"id-server-port"`
RelayServerPort int `mapstructure:"relay-server-port"`
}
type Config struct {
Lang string `mapstructure:"lang"`
App App
Admin Admin
Gorm Gorm
Mysql Mysql
Gin Gin
Logger Logger
Redis Redis
Cache Cache
Oss Oss
Jwt Jwt
Rustdesk Rustdesk
Proxy Proxy
Lang string `mapstructure:"lang"`
App App
Admin Admin
Gorm Gorm
Mysql Mysql
Postgresql Postgresql
Gin Gin
Logger Logger
Redis Redis
Cache Cache
Oss Oss
Jwt Jwt
Rustdesk Rustdesk
Proxy Proxy
Ldap Ldap
}
func (a *Admin) Init() {
if a.IdServerPort == 0 {
a.IdServerPort = DefaultIdServerPort
}
if a.RelayServerPort == 0 {
a.RelayServerPort = DefaultRelayServerPort
}
}
// Init 初始化配置
@@ -55,21 +73,26 @@ func Init(rowVal *Config, path string) *viper.Viper {
if err != nil {
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}
v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
//配置文件修改监听
fmt.Println("config file changed:", e.Name)
if err2 := v.Unmarshal(rowVal); err2 != nil {
fmt.Println(err2)
}
rowVal.Rustdesk.LoadKeyFile()
rowVal.Rustdesk.ParsePort()
})
/*
v.WatchConfig()
//监听配置修改没什么必要
v.OnConfigChange(func(e fsnotify.Event) {
//配置文件修改监听
fmt.Println("config file changed:", e.Name)
if err2 := v.Unmarshal(rowVal); err2 != nil {
fmt.Println(err2)
}
rowVal.Rustdesk.LoadKeyFile()
rowVal.Rustdesk.ParsePort()
})
*/
if err := v.Unmarshal(rowVal); err != nil {
fmt.Println(err)
panic(fmt.Errorf("Fatal error config: %s \n", err))
}
rowVal.Rustdesk.LoadKeyFile()
rowVal.Rustdesk.ParsePort()
rowVal.Admin.Init()
return v
}

View File

@@ -1,8 +1,9 @@
package config
const (
TypeSqlite = "sqlite"
TypeMysql = "mysql"
TypeSqlite = "sqlite"
TypeMysql = "mysql"
TypePostgresql = "postgresql"
)
type Gorm struct {
@@ -17,3 +18,13 @@ type Mysql struct {
Password string `mapstructure:"password"`
Dbname string `mapstructure:"dbname"`
}
type Postgresql struct {
Host string `mapstructure:"host"`
Port string `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Dbname string `mapstructure:"dbname"`
Sslmode string `mapstructure:"sslmode"` // "disable", "require", "verify-ca", "verify-full"
TimeZone string `mapstructure:"time-zone"` // e.g., "Asia/Shanghai"
}

36
config/ldap.go Normal file
View File

@@ -0,0 +1,36 @@
package config
type LdapUser struct {
BaseDn string `mapstructure:"base-dn"` // The base DN of the user for searching
EnableAttr string `mapstructure:"enable-attr"` // The attribute name of the user for enabling, in AD it is "userAccountControl", empty means no enable attribute, all users are enabled
EnableAttrValue string `mapstructure:"enable-attr-value"` // The value of the enable attribute when the user is enabled. If you are using AD, just leave it random str, it will be ignored.
Filter string `mapstructure:"filter"`
Username string `mapstructure:"username"`
Email string `mapstructure:"email"`
FirstName string `mapstructure:"first-name"`
LastName string `mapstructure:"last-name"`
Sync bool `mapstructure:"sync"` // Will sync the user's information to the internal database
AdminGroup string `mapstructure:"admin-group"` // Which group is the admin group
}
// type LdapGroup struct {
// BaseDn string `mapstructure:"base-dn"` // The base DN of the group for searching
// Name string `mapstructure:"name"` // The attribute name of the group
// Filter string `mapstructure:"filter"`
// Admin string `mapstructure:"admin"` // Which group is the admin group
// Member string `mapstructure:"member"` // How to get the member of the group: member, uniqueMember, or memberOf (default: member)
// Mode string `mapstructure:"mode"`
// Map map[string]string `mapstructure:"map"` // If mode is "map", map the LDAP group to the internal group
// }
type Ldap struct {
Enable bool `mapstructure:"enable"`
Url string `mapstructure:"url"`
TlsCaFile string `mapstructure:"tls-ca-file"`
TlsVerify bool `mapstructure:"tls-verify"`
BaseDn string `mapstructure:"base-dn"`
BindDn string `mapstructure:"bind-dn"`
BindPassword string `mapstructure:"bind-password"`
User LdapUser `mapstructure:"user"`
// Group LdapGroup `mapstructure:"group"`
}

View File

@@ -17,4 +17,10 @@ type OidcOauth struct {
ClientId string `mapstructure:"client-id"`
ClientSecret string `mapstructure:"client-secret"`
RedirectUrl string `mapstructure:"redirect-url"`
}
}
type LinuxdoOauth struct {
ClientId string `mapstructure:"client-id"`
ClientSecret string `mapstructure:"client-secret"`
RedirectUrl string `mapstructure:"redirect-url"`
}

View File

@@ -2,8 +2,6 @@ package config
import (
"os"
"strconv"
"strings"
)
const (
@@ -21,7 +19,8 @@ type Rustdesk struct {
KeyFile string `mapstructure:"key-file"`
Personal int `mapstructure:"personal"`
//webclient-magic-queryonline
WebclientMagicQueryonline int `mapstructure:"webclient-magic-queryonline"`
WebclientMagicQueryonline int `mapstructure:"webclient-magic-queryonline"`
WsHost string `mapstructure:"ws-host"`
}
func (rd *Rustdesk) LoadKeyFile() {
@@ -39,19 +38,3 @@ func (rd *Rustdesk) LoadKeyFile() {
return
}
}
func (rd *Rustdesk) ParsePort() {
// Parse port
idres := strings.Split(rd.IdServer, ":")
if len(idres) == 1 {
rd.IdServerPort = DefaultIdServerPort
} else if len(idres) == 2 {
rd.IdServerPort, _ = strconv.Atoi(idres[1])
}
relayres := strings.Split(rd.RelayServer, ":")
if len(relayres) == 1 {
rd.RelayServerPort = DefaultRelayServerPort
} else if len(relayres) == 2 {
rd.RelayServerPort, _ = strconv.Atoi(relayres[1])
}
}

0
data/.gitkeep Normal file
View File

View File

@@ -1407,6 +1407,280 @@ const docTemplateadmin = `{
}
}
},
"/admin/device_group/create": {
"post": {
"security": [
{
"token": []
}
],
"description": "创建设备群组",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"设备群组"
],
"summary": "创建设备群组",
"parameters": [
{
"description": "设备群组信息",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/admin.DeviceGroupForm"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/model.DeviceGroup"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/device_group/delete": {
"post": {
"security": [
{
"token": []
}
],
"description": "设备群组删除",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"设备群组"
],
"summary": "设备群组删除",
"parameters": [
{
"description": "群组信息",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/admin.DeviceGroupForm"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/device_group/detail/{id}": {
"get": {
"security": [
{
"token": []
}
],
"description": "设备群组详情",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"设备群组"
],
"summary": "设备群组详情",
"parameters": [
{
"type": "integer",
"description": "ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/model.Group"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/device_group/list": {
"get": {
"security": [
{
"token": []
}
],
"description": "群组列表",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"群组"
],
"summary": "群组列表",
"parameters": [
{
"type": "integer",
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "页大小",
"name": "page_size",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/model.GroupList"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/device_group/update": {
"post": {
"security": [
{
"token": []
}
],
"description": "设备群组编辑",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"设备群组"
],
"summary": "设备群组编辑",
"parameters": [
{
"description": "群组信息",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/admin.DeviceGroupForm"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/model.Group"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/file/oss_token": {
"get": {
"security": [
@@ -1783,7 +2057,7 @@ const docTemplateadmin = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/Gwen_http_request_admin.Login"
"$ref": "#/definitions/github_com_lejianwen_rustdesk-api_v2_http_request_admin.Login"
}
}
],
@@ -5104,27 +5378,6 @@ const docTemplateadmin = `{
}
},
"definitions": {
"Gwen_http_request_admin.Login": {
"type": "object",
"required": [
"password",
"username"
],
"properties": {
"captcha": {
"type": "string"
},
"password": {
"type": "string"
},
"platform": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"admin.AddressBookForm": {
"type": "object",
"required": [
@@ -5240,6 +5493,20 @@ const docTemplateadmin = `{
}
}
},
"admin.DeviceGroupForm": {
"type": "object",
"required": [
"name"
],
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
}
}
},
"admin.GroupForm": {
"type": "object",
"required": [
@@ -5327,6 +5594,12 @@ const docTemplateadmin = `{
"op": {
"type": "string"
},
"pkce_enable": {
"type": "boolean"
},
"pkce_method": {
"type": "string"
},
"redirect_url": {
"type": "string"
},
@@ -5355,6 +5628,9 @@ const docTemplateadmin = `{
"cpu": {
"type": "string"
},
"group_id": {
"type": "integer"
},
"hostname": {
"type": "string"
},
@@ -5542,6 +5818,30 @@ const docTemplateadmin = `{
}
}
},
"github_com_lejianwen_rustdesk-api_v2_http_request_admin.Login": {
"type": "object",
"required": [
"password",
"username"
],
"properties": {
"captcha": {
"type": "string"
},
"captcha_id": {
"type": "string"
},
"password": {
"type": "string"
},
"platform": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"model.AddressBook": {
"type": "object",
"properties": {
@@ -5842,6 +6142,23 @@ const docTemplateadmin = `{
}
}
},
"model.DeviceGroup": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"model.Group": {
"type": "object",
"properties": {
@@ -5973,6 +6290,12 @@ const docTemplateadmin = `{
"op": {
"type": "string"
},
"pkce_enable": {
"type": "boolean"
},
"pkce_method": {
"type": "string"
},
"redirect_url": {
"type": "string"
},
@@ -6013,6 +6336,9 @@ const docTemplateadmin = `{
"created_at": {
"type": "string"
},
"group_id": {
"type": "integer"
},
"hostname": {
"type": "string"
},

View File

@@ -1400,6 +1400,280 @@
}
}
},
"/admin/device_group/create": {
"post": {
"security": [
{
"token": []
}
],
"description": "创建设备群组",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"设备群组"
],
"summary": "创建设备群组",
"parameters": [
{
"description": "设备群组信息",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/admin.DeviceGroupForm"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/model.DeviceGroup"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/device_group/delete": {
"post": {
"security": [
{
"token": []
}
],
"description": "设备群组删除",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"设备群组"
],
"summary": "设备群组删除",
"parameters": [
{
"description": "群组信息",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/admin.DeviceGroupForm"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/device_group/detail/{id}": {
"get": {
"security": [
{
"token": []
}
],
"description": "设备群组详情",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"设备群组"
],
"summary": "设备群组详情",
"parameters": [
{
"type": "integer",
"description": "ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/model.Group"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/device_group/list": {
"get": {
"security": [
{
"token": []
}
],
"description": "群组列表",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"群组"
],
"summary": "群组列表",
"parameters": [
{
"type": "integer",
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "页大小",
"name": "page_size",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/model.GroupList"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/device_group/update": {
"post": {
"security": [
{
"token": []
}
],
"description": "设备群组编辑",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"设备群组"
],
"summary": "设备群组编辑",
"parameters": [
{
"description": "群组信息",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/admin.DeviceGroupForm"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/model.Group"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/file/oss_token": {
"get": {
"security": [
@@ -1776,7 +2050,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/Gwen_http_request_admin.Login"
"$ref": "#/definitions/github_com_lejianwen_rustdesk-api_v2_http_request_admin.Login"
}
}
],
@@ -5097,27 +5371,6 @@
}
},
"definitions": {
"Gwen_http_request_admin.Login": {
"type": "object",
"required": [
"password",
"username"
],
"properties": {
"captcha": {
"type": "string"
},
"password": {
"type": "string"
},
"platform": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"admin.AddressBookForm": {
"type": "object",
"required": [
@@ -5233,6 +5486,20 @@
}
}
},
"admin.DeviceGroupForm": {
"type": "object",
"required": [
"name"
],
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
}
}
},
"admin.GroupForm": {
"type": "object",
"required": [
@@ -5320,6 +5587,12 @@
"op": {
"type": "string"
},
"pkce_enable": {
"type": "boolean"
},
"pkce_method": {
"type": "string"
},
"redirect_url": {
"type": "string"
},
@@ -5348,6 +5621,9 @@
"cpu": {
"type": "string"
},
"group_id": {
"type": "integer"
},
"hostname": {
"type": "string"
},
@@ -5535,6 +5811,30 @@
}
}
},
"github_com_lejianwen_rustdesk-api_v2_http_request_admin.Login": {
"type": "object",
"required": [
"password",
"username"
],
"properties": {
"captcha": {
"type": "string"
},
"captcha_id": {
"type": "string"
},
"password": {
"type": "string"
},
"platform": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"model.AddressBook": {
"type": "object",
"properties": {
@@ -5835,6 +6135,23 @@
}
}
},
"model.DeviceGroup": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"model.Group": {
"type": "object",
"properties": {
@@ -5966,6 +6283,12 @@
"op": {
"type": "string"
},
"pkce_enable": {
"type": "boolean"
},
"pkce_method": {
"type": "string"
},
"redirect_url": {
"type": "string"
},
@@ -6006,6 +6329,9 @@
"created_at": {
"type": "string"
},
"group_id": {
"type": "integer"
},
"hostname": {
"type": "string"
},

View File

@@ -1,19 +1,5 @@
basePath: /api
definitions:
Gwen_http_request_admin.Login:
properties:
captcha:
type: string
password:
type: string
platform:
type: string
username:
type: string
required:
- password
- username
type: object
admin.AddressBookForm:
properties:
alias:
@@ -91,6 +77,15 @@ definitions:
- new_password
- old_password
type: object
admin.DeviceGroupForm:
properties:
id:
type: integer
name:
type: string
required:
- name
type: object
admin.GroupForm:
properties:
id:
@@ -144,6 +139,10 @@ definitions:
type: string
op:
type: string
pkce_enable:
type: boolean
pkce_method:
type: string
redirect_url:
type: string
scopes:
@@ -167,6 +166,8 @@ definitions:
properties:
cpu:
type: string
group_id:
type: integer
hostname:
type: string
id:
@@ -292,6 +293,22 @@ definitions:
required:
- ids
type: object
github_com_lejianwen_rustdesk-api_v2_http_request_admin.Login:
properties:
captcha:
type: string
captcha_id:
type: string
password:
type: string
platform:
type: string
username:
type: string
required:
- password
- username
type: object
model.AddressBook:
properties:
alias:
@@ -492,6 +509,17 @@ definitions:
total:
type: integer
type: object
model.DeviceGroup:
properties:
created_at:
type: string
id:
type: integer
name:
type: string
updated_at:
type: string
type: object
model.Group:
properties:
created_at:
@@ -579,6 +607,10 @@ definitions:
type: string
op:
type: string
pkce_enable:
type: boolean
pkce_method:
type: string
redirect_url:
type: string
scopes:
@@ -605,6 +637,8 @@ definitions:
type: string
created_at:
type: string
group_id:
type: integer
hostname:
type: string
id:
@@ -1610,6 +1644,167 @@ paths:
summary: RUSTDESK服务配置
tags:
- ADMIN
/admin/device_group/create:
post:
consumes:
- application/json
description: 创建设备群组
parameters:
- description: 设备群组信息
in: body
name: body
required: true
schema:
$ref: '#/definitions/admin.DeviceGroupForm'
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.Response'
- properties:
data:
$ref: '#/definitions/model.DeviceGroup'
type: object
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
security:
- token: []
summary: 创建设备群组
tags:
- 设备群组
/admin/device_group/delete:
post:
consumes:
- application/json
description: 设备群组删除
parameters:
- description: 群组信息
in: body
name: body
required: true
schema:
$ref: '#/definitions/admin.DeviceGroupForm'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
security:
- token: []
summary: 设备群组删除
tags:
- 设备群组
/admin/device_group/detail/{id}:
get:
consumes:
- application/json
description: 设备群组详情
parameters:
- description: ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.Response'
- properties:
data:
$ref: '#/definitions/model.Group'
type: object
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
security:
- token: []
summary: 设备群组详情
tags:
- 设备群组
/admin/device_group/list:
get:
consumes:
- application/json
description: 群组列表
parameters:
- description: 页码
in: query
name: page
type: integer
- description: 页大小
in: query
name: page_size
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.Response'
- properties:
data:
$ref: '#/definitions/model.GroupList'
type: object
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
security:
- token: []
summary: 群组列表
tags:
- 群组
/admin/device_group/update:
post:
consumes:
- application/json
description: 设备群组编辑
parameters:
- description: 群组信息
in: body
name: body
required: true
schema:
$ref: '#/definitions/admin.DeviceGroupForm'
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.Response'
- properties:
data:
$ref: '#/definitions/model.Group'
type: object
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
security:
- token: []
summary: 设备群组编辑
tags:
- 设备群组
/admin/file/oss_token:
get:
consumes:
@@ -1830,7 +2025,7 @@ paths:
name: body
required: true
schema:
$ref: '#/definitions/Gwen_http_request_admin.Login'
$ref: '#/definitions/github_com_lejianwen_rustdesk-api_v2_http_request_admin.Login'
produces:
- application/json
responses:

View File

@@ -767,6 +767,66 @@ const docTemplateapi = `{
}
}
},
"/device-group/accessible": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "机器",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"群组"
],
"summary": "设备",
"parameters": [
{
"type": "integer",
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "每页数量",
"name": "pageSize",
"in": "query"
},
{
"type": "integer",
"description": "状态",
"name": "status",
"in": "query"
},
{
"type": "string",
"description": "accessible",
"name": "accessible",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.DataResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/heartbeat": {
"post": {
"description": "心跳",
@@ -1148,7 +1208,7 @@ const docTemplateapi = `{
"application/json"
],
"tags": [
"地址"
"System"
],
"summary": "提交系统信息",
"parameters": [
@@ -1178,6 +1238,35 @@ const docTemplateapi = `{
}
}
},
"/sysinfo_ver": {
"post": {
"description": "获取系统版本信息",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"System"
],
"summary": "获取系统版本信息",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/users": {
"get": {
"security": [

View File

@@ -760,6 +760,66 @@
}
}
},
"/device-group/accessible": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "机器",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"群组"
],
"summary": "设备",
"parameters": [
{
"type": "integer",
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "每页数量",
"name": "pageSize",
"in": "query"
},
{
"type": "integer",
"description": "状态",
"name": "status",
"in": "query"
},
{
"type": "string",
"description": "accessible",
"name": "accessible",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.DataResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/heartbeat": {
"post": {
"description": "心跳",
@@ -1141,7 +1201,7 @@
"application/json"
],
"tags": [
"地址"
"System"
],
"summary": "提交系统信息",
"parameters": [
@@ -1171,6 +1231,35 @@
}
}
},
"/sysinfo_ver": {
"post": {
"description": "获取系统版本信息",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"System"
],
"summary": "获取系统版本信息",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/users": {
"get": {
"security": [

View File

@@ -671,6 +671,44 @@ paths:
summary: 用户信息
tags:
- 用户
/device-group/accessible:
get:
consumes:
- application/json
description: 机器
parameters:
- description: 页码
in: query
name: page
type: integer
- description: 每页数量
in: query
name: pageSize
type: integer
- description: 状态
in: query
name: status
type: integer
- description: accessible
in: query
name: accessible
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.DataResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
security:
- BearerAuth: []
summary: 设备
tags:
- 群组
/heartbeat:
post:
consumes:
@@ -935,7 +973,26 @@ paths:
$ref: '#/definitions/response.ErrorResponse'
summary: 提交系统信息
tags:
- 地址
- System
/sysinfo_ver:
post:
consumes:
- application/json
description: 获取系统版本信息
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: 获取系统版本信息
tags:
- System
/users:
get:
consumes:

BIN
docs/init_admin_pwd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1,4 +1,4 @@
package Gwen
package main
//go:generate swag init -g cmd/apimain.go --output docs/api --instanceName api --exclude http/controller/admin
//go:generate swag init -g cmd/apimain.go --output docs/admin --instanceName admin --exclude http/controller/api

View File

@@ -1,3 +1,3 @@
package Gwen
package main
//go:generate go run cmd/apimain.go

View File

@@ -14,6 +14,7 @@ import (
en_translations "github.com/go-playground/validator/v10/translations/en"
es_translations "github.com/go-playground/validator/v10/translations/es"
fr_translations "github.com/go-playground/validator/v10/translations/fr"
ko_translations "github.com/go-playground/validator/v10/translations/ko"
ru_translations "github.com/go-playground/validator/v10/translations/ru"
zh_translations "github.com/go-playground/validator/v10/translations/zh"
zh_tw_translations "github.com/go-playground/validator/v10/translations/zh_tw"
@@ -51,8 +52,7 @@ func ApiInitValidator() {
panic(err)
}
//validate没有ko的翻译使用zh的翻译
err = zh_translations.RegisterDefaultTranslations(validate, koTrans)
err = ko_translations.RegisterDefaultTranslations(validate, koTrans)
if err != nil {
panic(err)
}

View File

@@ -1,15 +1,16 @@
package global
import (
"Gwen/config"
"Gwen/lib/cache"
"Gwen/lib/jwt"
"Gwen/lib/lock"
"Gwen/lib/upload"
"github.com/gin-gonic/gin"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
"github.com/go-redis/redis/v8"
"github.com/lejianwen/rustdesk-api/v2/config"
"github.com/lejianwen/rustdesk-api/v2/lib/cache"
"github.com/lejianwen/rustdesk-api/v2/lib/jwt"
"github.com/lejianwen/rustdesk-api/v2/lib/lock"
"github.com/lejianwen/rustdesk-api/v2/lib/upload"
"github.com/lejianwen/rustdesk-api/v2/utils"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
@@ -31,8 +32,9 @@ var (
ValidStruct func(*gin.Context, interface{}) []string
ValidVar func(ctx *gin.Context, field interface{}, tag string) []string
}
Oss *upload.Oss
Jwt *jwt.Jwt
Lock lock.Locker
Localizer func(lang string) *i18n.Localizer
Oss *upload.Oss
Jwt *jwt.Jwt
Lock lock.Locker
Localizer func(lang string) *i18n.Localizer
LoginLimiter *utils.LoginLimiter
)

View File

@@ -15,7 +15,6 @@ func InitI18n() {
fileInfos, err := os.ReadDir(dir)
if err != nil {
panic(err)
return
}
for _, fileInfo := range fileInfos {
//如果文件名不是.toml结尾

41
go.mod
View File

@@ -1,19 +1,23 @@
module Gwen
module github.com/lejianwen/rustdesk-api/v2
go 1.22
go 1.23
toolchain go1.23.10
require (
github.com/BurntSushi/toml v1.3.2
github.com/antonfisher/nested-logrus-formatter v1.3.1
github.com/fsnotify/fsnotify v1.5.1
github.com/coreos/go-oidc/v3 v3.12.0
github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6
github.com/gin-gonic/gin v1.9.0
github.com/go-ldap/ldap/v3 v3.4.10
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.11.2
github.com/go-playground/validator/v10 v10.26.0
github.com/go-redis/redis/v8 v8.11.4
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.1.2
github.com/google/uuid v1.6.0
github.com/mojocn/base64Captcha v1.3.6
github.com/nicksnyder/go-i18n/v2 v2.4.0
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.8.1
@@ -22,13 +26,15 @@ require (
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.3
golang.org/x/oauth2 v0.23.0
golang.org/x/text v0.18.0
golang.org/x/text v0.22.0
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlite v1.5.6
gorm.io/gorm v1.25.7
gorm.io/gorm v1.25.10
)
require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
@@ -36,7 +42,11 @@ require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
@@ -46,12 +56,16 @@ require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
@@ -59,9 +73,9 @@ require (
github.com/mitchellh/mapstructure v1.4.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mojocn/base64Captcha v1.3.6 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
@@ -70,11 +84,12 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/image v0.13.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/tools v0.26.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/ini.v1 v1.63.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

View File

@@ -1,13 +1,13 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/service"
"encoding/json"
_ "encoding/json"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/service"
"gorm.io/gorm"
"strconv"
)

View File

@@ -1,12 +1,12 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service"
"gorm.io/gorm"
"strconv"
)

View File

@@ -1,12 +1,12 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service"
"gorm.io/gorm"
"strconv"
)
@@ -120,7 +120,7 @@ func (abcr *AddressBookCollectionRule) CheckForm(t *model.AddressBookCollectionR
//check to_id
if t.Type == model.ShareAddressBookRuleTypePersonal {
if t.ToId == t.UserId {
return "ParamsError", false
return "CannotShareToSelf", false
}
tou := service.AllService.UserService.InfoById(t.ToId)
if tou.Id == 0 {
@@ -135,7 +135,7 @@ func (abcr *AddressBookCollectionRule) CheckForm(t *model.AddressBookCollectionR
return "ParamsError", false
}
// 重复检查
ex := service.AllService.AddressBookService.RulePersonalInfoByToIdAndCid(t.ToId, t.CollectionId)
ex := service.AllService.AddressBookService.RuleInfoByToIdAndCid(t.Type, t.ToId, t.CollectionId)
if t.Id == 0 && ex.Id > 0 {
return "ItemExists", false
}

View File

@@ -1,12 +1,12 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service"
"gorm.io/gorm"
)

View File

@@ -1,10 +1,11 @@
package admin
import (
"Gwen/global"
"Gwen/http/response"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service"
"os"
"strings"
)
@@ -60,13 +61,30 @@ func (co *Config) AppConfig(c *gin.Context) {
// @Security token
func (co *Config) AdminConfig(c *gin.Context) {
u := service.AllService.UserService.CurUser(c)
u := &model.User{}
token := c.GetHeader("api-token")
if token != "" {
u, _ = service.AllService.UserService.InfoByAccessToken(token)
if !service.AllService.UserService.CheckUserEnable(u) {
u.Id = 0
}
}
if u.Id == 0 {
response.Success(c, &gin.H{
"title": global.Config.Admin.Title,
})
return
}
hello := global.Config.Admin.Hello
helloFile := global.Config.Admin.HelloFile
if helloFile != "" {
b, err := os.ReadFile(helloFile)
if err == nil && len(b) > 0 {
hello = string(b)
if hello == "" {
helloFile := global.Config.Admin.HelloFile
if helloFile != "" {
b, err := os.ReadFile(helloFile)
if err == nil && len(b) > 0 {
hello = string(b)
}
}
}

View File

@@ -0,0 +1,160 @@
package admin
import (
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/service"
"strconv"
)
type DeviceGroup struct {
}
// Detail 设备群组
// @Tags 设备群组
// @Summary 设备群组详情
// @Description 设备群组详情
// @Accept json
// @Produce json
// @Param id path int true "ID"
// @Success 200 {object} response.Response{data=model.Group}
// @Failure 500 {object} response.Response
// @Router /admin/device_group/detail/{id} [get]
// @Security token
func (ct *DeviceGroup) Detail(c *gin.Context) {
id := c.Param("id")
iid, _ := strconv.Atoi(id)
u := service.AllService.GroupService.DeviceGroupInfoById(uint(iid))
if u.Id > 0 {
response.Success(c, u)
return
}
response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound"))
return
}
// Create 创建设备群组
// @Tags 设备群组
// @Summary 创建设备群组
// @Description 创建设备群组
// @Accept json
// @Produce json
// @Param body body admin.DeviceGroupForm true "设备群组信息"
// @Success 200 {object} response.Response{data=model.DeviceGroup}
// @Failure 500 {object} response.Response
// @Router /admin/device_group/create [post]
// @Security token
func (ct *DeviceGroup) Create(c *gin.Context) {
f := &admin.DeviceGroupForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
return
}
errList := global.Validator.ValidStruct(c, f)
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
u := f.ToDeviceGroup()
err := service.AllService.GroupService.DeviceGroupCreate(u)
if err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "OperationFailed")+err.Error())
return
}
response.Success(c, nil)
}
// List 列表
// @Tags 群组
// @Summary 群组列表
// @Description 群组列表
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param page_size query int false "页大小"
// @Success 200 {object} response.Response{data=model.GroupList}
// @Failure 500 {object} response.Response
// @Router /admin/device_group/list [get]
// @Security token
func (ct *DeviceGroup) List(c *gin.Context) {
query := &admin.PageQuery{}
if err := c.ShouldBindQuery(query); err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
return
}
res := service.AllService.GroupService.DeviceGroupList(query.Page, query.PageSize, nil)
response.Success(c, res)
}
// Update 编辑
// @Tags 设备群组
// @Summary 设备群组编辑
// @Description 设备群组编辑
// @Accept json
// @Produce json
// @Param body body admin.DeviceGroupForm true "群组信息"
// @Success 200 {object} response.Response{data=model.Group}
// @Failure 500 {object} response.Response
// @Router /admin/device_group/update [post]
// @Security token
func (ct *DeviceGroup) Update(c *gin.Context) {
f := &admin.DeviceGroupForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
return
}
if f.Id == 0 {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError"))
return
}
errList := global.Validator.ValidStruct(c, f)
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
u := f.ToDeviceGroup()
err := service.AllService.GroupService.DeviceGroupUpdate(u)
if err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "OperationFailed")+err.Error())
return
}
response.Success(c, nil)
}
// Delete 删除
// @Tags 设备群组
// @Summary 设备群组删除
// @Description 设备群组删除
// @Accept json
// @Produce json
// @Param body body admin.DeviceGroupForm true "群组信息"
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/device_group/delete [post]
// @Security token
func (ct *DeviceGroup) Delete(c *gin.Context) {
f := &admin.DeviceGroupForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
return
}
id := f.Id
errList := global.Validator.ValidVar(c, id, "required,gt=0")
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
u := service.AllService.GroupService.DeviceGroupInfoById(f.Id)
if u.Id > 0 {
err := service.AllService.GroupService.DeviceGroupDelete(u)
if err == nil {
response.Success(c, nil)
return
}
response.Fail(c, 101, response.TranslateMsg(c, "OperationFailed")+err.Error())
return
}
response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound"))
}

View File

@@ -1,11 +1,11 @@
package admin
import (
"Gwen/global"
"Gwen/http/response"
"Gwen/lib/upload"
"fmt"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/lib/upload"
"os"
"time"
)

View File

@@ -1,11 +1,11 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/service"
"strconv"
)

View File

@@ -1,145 +1,21 @@
package admin
import (
"Gwen/global"
"Gwen/http/controller/api"
"Gwen/http/request/admin"
apiReq "Gwen/http/request/api"
"Gwen/http/response"
adResp "Gwen/http/response/admin"
"Gwen/model"
"Gwen/service"
"fmt"
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
"sync"
"time"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/controller/api"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
apiReq "github.com/lejianwen/rustdesk-api/v2/http/request/api"
"github.com/lejianwen/rustdesk-api/v2/http/response"
adResp "github.com/lejianwen/rustdesk-api/v2/http/response/admin"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service"
)
type Login struct {
}
// Captcha 验证码结构
type Captcha struct {
Id string `json:"id"` // 验证码 ID
B64 string `json:"b64"` // base64 验证码
Code string `json:"-"` // 验证码内容
ExpiresAt time.Time `json:"-"` // 过期时间
}
type LoginLimiter struct {
mu sync.RWMutex
failCount map[string]int // 记录每个 IP 的失败次数
timestamp map[string]time.Time // 记录每个 IP 的最后失败时间
captchas map[string]Captcha // 每个 IP 的验证码
threshold int // 失败阈值
expiry time.Duration // 失败记录过期时间
}
func NewLoginLimiter(threshold int, expiry time.Duration) *LoginLimiter {
return &LoginLimiter{
failCount: make(map[string]int),
timestamp: make(map[string]time.Time),
captchas: make(map[string]Captcha),
threshold: threshold,
expiry: expiry,
}
}
// RecordFailure 记录登录失败
func (l *LoginLimiter) RecordFailure(ip string) {
l.mu.Lock()
defer l.mu.Unlock()
// 如果该 IP 的记录已经过期,重置计数
if lastTime, exists := l.timestamp[ip]; exists && time.Since(lastTime) > l.expiry {
l.failCount[ip] = 0
}
// 更新失败次数和时间戳
l.failCount[ip]++
l.timestamp[ip] = time.Now()
}
// NeedsCaptcha 检查是否需要验证码
func (l *LoginLimiter) NeedsCaptcha(ip string) bool {
l.mu.RLock()
defer l.mu.RUnlock()
// 检查记录是否存在且未过期
if lastTime, exists := l.timestamp[ip]; exists && time.Since(lastTime) <= l.expiry {
return l.failCount[ip] >= l.threshold
}
return false
}
// GenerateCaptcha 为指定 IP 生成验证码
func (l *LoginLimiter) GenerateCaptcha(ip string) Captcha {
l.mu.Lock()
defer l.mu.Unlock()
capd := base64Captcha.NewDriverString(50, 150, 5, 10, 4, "1234567890abcdefghijklmnopqrstuvwxyz", nil, nil, nil)
b64cap := base64Captcha.NewCaptcha(capd, base64Captcha.DefaultMemStore)
id, b64s, answer, err := b64cap.Generate()
if err != nil {
global.Logger.Error("Generate captcha failed: " + err.Error())
return Captcha{}
}
// 保存验证码到对应 IP
l.captchas[ip] = Captcha{
Id: id,
B64: b64s,
Code: answer,
ExpiresAt: time.Now().Add(5 * time.Minute),
}
return l.captchas[ip]
}
// VerifyCaptcha 验证指定 IP 的验证码
func (l *LoginLimiter) VerifyCaptcha(ip, code string) bool {
l.mu.RLock()
defer l.mu.RUnlock()
// 检查验证码是否存在且未过期
if captcha, exists := l.captchas[ip]; exists && time.Now().Before(captcha.ExpiresAt) {
return captcha.Code == code
}
return false
}
// RemoveCaptcha 移除指定 IP 的验证码
func (l *LoginLimiter) RemoveCaptcha(ip string) {
l.mu.Lock()
defer l.mu.Unlock()
delete(l.captchas, ip)
}
// CleanupExpired 清理过期的记录
func (l *LoginLimiter) CleanupExpired() {
l.mu.Lock()
defer l.mu.Unlock()
now := time.Now()
for ip, lastTime := range l.timestamp {
if now.Sub(lastTime) > l.expiry {
delete(l.failCount, ip)
delete(l.timestamp, ip)
delete(l.captchas, ip)
}
}
}
func (l *LoginLimiter) RemoveRecord(ip string) {
l.mu.Lock()
defer l.mu.Unlock()
delete(l.failCount, ip)
delete(l.timestamp, ip)
delete(l.captchas, ip)
}
var loginLimiter = NewLoginLimiter(3, 5*time.Minute)
// Login 登录
// @Tags 登录
// @Summary 登录
@@ -152,10 +28,20 @@ var loginLimiter = NewLoginLimiter(3, 5*time.Minute)
// @Router /admin/login [post]
// @Security token
func (ct *Login) Login(c *gin.Context) {
if global.Config.App.DisablePwdLogin {
response.Fail(c, 101, response.TranslateMsg(c, "PwdLoginDisabled"))
return
}
// 检查登录限制
loginLimiter := global.LoginLimiter
clientIp := c.ClientIP()
_, needCaptcha := loginLimiter.CheckSecurityStatus(clientIp)
f := &admin.Login{}
err := c.ShouldBindJSON(f)
clientIp := c.ClientIP()
if err != nil {
loginLimiter.RecordFailedAttempt(clientIp)
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "ParamsError", c.RemoteIP(), clientIp))
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
return
@@ -163,14 +49,15 @@ func (ct *Login) Login(c *gin.Context) {
errList := global.Validator.ValidStruct(c, f)
if len(errList) > 0 {
loginLimiter.RecordFailedAttempt(clientIp)
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "ParamsError", c.RemoteIP(), clientIp))
response.Fail(c, 101, errList[0])
return
}
// 检查是否需要验证码
if loginLimiter.NeedsCaptcha(clientIp) {
if f.Captcha == "" || !loginLimiter.VerifyCaptcha(clientIp, f.Captcha) {
if needCaptcha {
if f.CaptchaId == "" || f.Captcha == "" || !loginLimiter.VerifyCaptcha(f.CaptchaId, f.Captcha) {
response.Fail(c, 101, response.TranslateMsg(c, "CaptchaError"))
return
}
@@ -180,14 +67,21 @@ func (ct *Login) Login(c *gin.Context) {
if u.Id == 0 {
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "UsernameOrPasswordError", c.RemoteIP(), clientIp))
loginLimiter.RecordFailure(clientIp)
if loginLimiter.NeedsCaptcha(clientIp) {
// 移除原验证码,重新生成
loginLimiter.RemoveCaptcha(clientIp)
loginLimiter.RecordFailedAttempt(clientIp)
if _, needCaptcha = loginLimiter.CheckSecurityStatus(clientIp); needCaptcha {
response.Fail(c, 110, response.TranslateMsg(c, "UsernameOrPasswordError"))
} else {
response.Fail(c, 101, response.TranslateMsg(c, "UsernameOrPasswordError"))
}
return
}
if !service.AllService.UserService.CheckUserEnable(u) {
if needCaptcha {
response.Fail(c, 110, response.TranslateMsg(c, "UserDisabled"))
return
}
response.Fail(c, 101, response.TranslateMsg(c, "UsernameOrPasswordError"))
response.Fail(c, 101, response.TranslateMsg(c, "UserDisabled"))
return
}
@@ -200,23 +94,37 @@ func (ct *Login) Login(c *gin.Context) {
Platform: f.Platform,
})
// 成功清除记录
loginLimiter.RemoveRecord(clientIp)
// 清理过期记录
go loginLimiter.CleanupExpired()
// 登录成功清除登录限制
loginLimiter.RemoveAttempts(clientIp)
responseLoginSuccess(c, u, ut.Token)
}
func (ct *Login) Captcha(c *gin.Context) {
loginLimiter := global.LoginLimiter
clientIp := c.ClientIP()
if !loginLimiter.NeedsCaptcha(clientIp) {
banned, needCaptcha := loginLimiter.CheckSecurityStatus(clientIp)
if banned {
response.Fail(c, 101, response.TranslateMsg(c, "LoginBanned"))
return
}
if !needCaptcha {
response.Fail(c, 101, response.TranslateMsg(c, "NoCaptchaRequired"))
return
}
captcha := loginLimiter.GenerateCaptcha(clientIp)
err, captcha := loginLimiter.RequireCaptcha()
if err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "CaptchaError")+err.Error())
return
}
err, b64 := loginLimiter.DrawCaptcha(captcha.Content)
if err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "CaptchaError")+err.Error())
return
}
response.Success(c, gin.H{
"captcha": captcha,
"captcha": gin.H{
"id": captcha.Id,
"b64": b64,
},
})
}
@@ -248,12 +156,18 @@ func (ct *Login) Logout(c *gin.Context) {
// @Failure 500 {object} response.ErrorResponse
// @Router /admin/login-options [post]
func (ct *Login) LoginOptions(c *gin.Context) {
ip := c.ClientIP()
loginLimiter := global.LoginLimiter
clientIp := c.ClientIP()
banned, needCaptcha := loginLimiter.CheckSecurityStatus(clientIp)
if banned {
response.Fail(c, 101, response.TranslateMsg(c, "LoginBanned"))
return
}
ops := service.AllService.OauthService.GetOauthProviders()
response.Success(c, gin.H{
"ops": ops,
"register": global.Config.App.Register,
"need_captcha": loginLimiter.NeedsCaptcha(ip),
"need_captcha": needCaptcha,
})
}
@@ -274,13 +188,13 @@ func (ct *Login) OidcAuth(c *gin.Context) {
return
}
err, code, url := service.AllService.OauthService.BeginAuth(f.Op)
err, state, verifier, nonce, url := service.AllService.OauthService.BeginAuth(f.Op)
if err != nil {
response.Error(c, response.TranslateMsg(c, err.Error()))
return
}
service.AllService.OauthService.SetOauthCache(code, &service.OauthCacheItem{
service.AllService.OauthService.SetOauthCache(state, &service.OauthCacheItem{
Action: service.OauthActionTypeLogin,
Op: f.Op,
Id: f.Id,
@@ -288,10 +202,12 @@ func (ct *Login) OidcAuth(c *gin.Context) {
// DeviceOs: ct.Platform(c),
DeviceOs: f.DeviceInfo.Os,
Uuid: f.Uuid,
Verifier: verifier,
Nonce: nonce,
}, 5*60)
response.Success(c, gin.H{
"code": code,
"code": state,
"url": url,
})
}

View File

@@ -1,12 +1,12 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service"
"gorm.io/gorm"
"strconv"
)

View File

@@ -1,12 +1,12 @@
package my
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/service"
"encoding/json"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/service"
"gorm.io/gorm"
)

View File

@@ -1,12 +1,12 @@
package my
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service"
"gorm.io/gorm"
)

View File

@@ -1,12 +1,12 @@
package my
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service"
"gorm.io/gorm"
)
@@ -100,21 +100,21 @@ func (abcr *AddressBookCollectionRule) CheckForm(u *model.User, t *model.Address
//check to_id
if t.Type == model.ShareAddressBookRuleTypePersonal {
if t.ToId == t.UserId {
return "ParamsError", false
return "CannotShareToSelf", false
}
tou := service.AllService.UserService.InfoById(t.ToId)
if tou.Id == 0 {
return "ItemNotFound", false
}
//非管理员不能分享给非本组织用户
if tou.GroupId != u.GroupId {
return "NoAccess", false
}
//if tou.GroupId != u.GroupId {
// return "NoAccess", false
//}
} else if t.Type == model.ShareAddressBookRuleTypeGroup {
//非管理员不能分享给其他组
if t.ToId != u.GroupId {
return "NoAccess", false
}
//if t.ToId != u.GroupId {
// return "NoAccess", false
//}
tog := service.AllService.GroupService.InfoById(t.ToId)
if tog.Id == 0 {
@@ -124,7 +124,7 @@ func (abcr *AddressBookCollectionRule) CheckForm(u *model.User, t *model.Address
return "ParamsError", false
}
// 重复检查
ex := service.AllService.AddressBookService.RulePersonalInfoByToIdAndCid(t.ToId, t.CollectionId)
ex := service.AllService.AddressBookService.RuleInfoByToIdAndCid(t.Type, t.ToId, t.CollectionId)
if t.Id == 0 && ex.Id > 0 {
return "ItemExists", false
}

View File

@@ -1,12 +1,12 @@
package my
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service"
"gorm.io/gorm"
)

View File

@@ -1,10 +1,10 @@
package my
import (
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/service"
"gorm.io/gorm"
"time"
)

View File

@@ -1,11 +1,11 @@
package my
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/service"
"gorm.io/gorm"
)

View File

@@ -1,11 +1,11 @@
package my
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/service"
"gorm.io/gorm"
)

View File

@@ -1,12 +1,12 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
adminReq "Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
adminReq "github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/service"
"strconv"
)
@@ -43,20 +43,22 @@ func (o *Oauth) ToBind(c *gin.Context) {
return
}
err, code, url := service.AllService.OauthService.BeginAuth(f.Op)
err, state, verifier, nonce, url := service.AllService.OauthService.BeginAuth(f.Op)
if err != nil {
response.Error(c, response.TranslateMsg(c, err.Error()))
return
}
service.AllService.OauthService.SetOauthCache(code, &service.OauthCacheItem{
Action: service.OauthActionTypeBind,
Op: f.Op,
UserId: u.Id,
service.AllService.OauthService.SetOauthCache(state, &service.OauthCacheItem{
Action: service.OauthActionTypeBind,
Op: f.Op,
UserId: u.Id,
Verifier: verifier,
Nonce: nonce,
}, 5*60)
response.Success(c, gin.H{
"code": code,
"code": state,
"url": url,
})
}

View File

@@ -1,11 +1,11 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/service"
"gorm.io/gorm"
"strconv"
"time"
@@ -108,6 +108,12 @@ func (ct *Peer) List(c *gin.Context) {
if query.Uuids != "" {
tx.Where("uuid in (?)", query.Uuids)
}
if query.Username != "" {
tx.Where("username like ?", "%"+query.Username+"%")
}
if query.Ip != "" {
tx.Where("last_online_ip like ?", "%"+query.Ip+"%")
}
})
response.Success(c, res)
}

View File

@@ -1,12 +1,12 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service"
)
type Rustdesk struct {
@@ -119,7 +119,16 @@ func (r *Rustdesk) SendCmd(c *gin.Context) {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError"))
return
}
res, err := service.AllService.ServerCmdService.SendCmd(rc.Target, rc.Cmd, rc.Option)
port := 0
switch rc.Target {
case model.ServerCmdTargetIdServer:
port = global.Config.Admin.IdServerPort - 1
case model.ServerCmdTargetRelayServer:
port = global.Config.Admin.RelayServerPort
}
res, err := service.AllService.ServerCmdService.SendCmd(port, rc.Cmd, rc.Option)
if err != nil {
response.Fail(c, 101, err.Error())
return

View File

@@ -1,11 +1,11 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/service"
"gorm.io/gorm"
)

View File

@@ -1,11 +1,11 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/service"
"gorm.io/gorm"
"strconv"
)

View File

@@ -1,13 +1,13 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
adResp "Gwen/http/response/admin"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
adResp "github.com/lejianwen/rustdesk-api/v2/http/response/admin"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service"
"gorm.io/gorm"
"strconv"
)
@@ -296,32 +296,12 @@ func (ct *User) MyOauth(c *gin.Context) {
// groupUsers
func (ct *User) GroupUsers(c *gin.Context) {
q := &admin.GroupUsersQuery{}
if err := c.ShouldBindJSON(q); err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
return
}
u := service.AllService.UserService.CurUser(c)
gid := u.GroupId
uid := u.Id
if service.AllService.UserService.IsAdmin(u) && q.UserId > 0 {
nu := service.AllService.UserService.InfoById(q.UserId)
gid = nu.GroupId
uid = q.UserId
}
res := service.AllService.UserService.List(1, 999, func(tx *gorm.DB) {
tx.Where("group_id = ?", gid)
aG := service.AllService.GroupService.List(1, 999, nil)
aU := service.AllService.UserService.List(1, 9999, nil)
response.Success(c, gin.H{
"groups": aG.Groups,
"users": aU.Users,
})
var data []*adResp.GroupUsersPayload
for _, _u := range res.Users {
gup := &adResp.GroupUsersPayload{}
gup.FromUser(_u)
if _u.Id == uid {
gup.Status = 0
}
data = append(data, gup)
}
response.Success(c, data)
}
// Register
@@ -340,11 +320,22 @@ func (ct *User) Register(c *gin.Context) {
response.Fail(c, 101, errList[0])
return
}
u := service.AllService.UserService.Register(f.Username, f.Email, f.Password)
regStatus := model.StatusCode(global.Config.App.RegisterStatus)
// 注册状态可能未配置,默认启用
if regStatus != model.COMMON_STATUS_DISABLED && regStatus != model.COMMON_STATUS_ENABLE {
regStatus = model.COMMON_STATUS_ENABLE
}
u := service.AllService.UserService.Register(f.Username, f.Email, f.Password, regStatus)
if u == nil || u.Id == 0 {
response.Fail(c, 101, response.TranslateMsg(c, "OperationFailed"))
return
}
if regStatus == model.COMMON_STATUS_DISABLED {
// 需要管理员审核
response.Fail(c, 101, response.TranslateMsg(c, "RegisterSuccessWaitAdminConfirm"))
return
}
// 注册成功后自动登录
ut := service.AllService.UserService.Login(u, &model.LoginLog{
UserId: u.Id,

View File

@@ -1,12 +1,12 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service"
"gorm.io/gorm"
)

View File

@@ -1,16 +1,16 @@
package api
import (
"Gwen/global"
requstform "Gwen/http/request/api"
"Gwen/http/response"
"Gwen/http/response/api"
"Gwen/model"
"Gwen/service"
"Gwen/utils"
"encoding/json"
"errors"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
requstform "github.com/lejianwen/rustdesk-api/v2/http/request/api"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/http/response/api"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service"
"github.com/lejianwen/rustdesk-api/v2/utils"
"net/http"
"strconv"
"strings"

View File

@@ -1,12 +1,12 @@
package api
import (
request "Gwen/http/request/api"
"Gwen/http/response"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
request "github.com/lejianwen/rustdesk-api/v2/http/request/api"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service"
"time"
)

View File

@@ -1,12 +1,12 @@
package api
import (
apiReq "Gwen/http/request/api"
"Gwen/http/response"
apiResp "Gwen/http/response/api"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin"
apiReq "github.com/lejianwen/rustdesk-api/v2/http/request/api"
"github.com/lejianwen/rustdesk-api/v2/http/response"
apiResp "github.com/lejianwen/rustdesk-api/v2/http/response/api"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service"
"net/http"
)
@@ -45,7 +45,7 @@ func (g *Group) Users(c *gin.Context) {
userList = service.AllService.UserService.ListByGroupId(u.GroupId, q.Page, q.PageSize)
}
var data []*apiResp.UserPayload
data := make([]*apiResp.UserPayload, 0, len(userList.Users))
for _, user := range userList.Users {
up := &apiResp.UserPayload{}
up.FromUser(user)
@@ -88,21 +88,30 @@ func (g *Group) Peers(c *gin.Context) {
users = service.AllService.UserService.ListIdAndNameByGroupId(u.GroupId)
}
namesById := make(map[uint]string)
userIds := make([]uint, 0)
namesById := make(map[uint]string, len(users))
userIds := make([]uint, 0, len(users))
for _, user := range users {
namesById[user.Id] = user.Username
userIds = append(userIds, user.Id)
}
dGroupNameById := make(map[uint]string)
allGroup := service.AllService.GroupService.DeviceGroupList(1, 999, nil)
for _, group := range allGroup.DeviceGroups {
dGroupNameById[group.Id] = group.Name
}
peerList := service.AllService.PeerService.ListByUserIds(userIds, q.Page, q.PageSize)
var data []*apiResp.GroupPeerPayload
data := make([]*apiResp.GroupPeerPayload, 0, len(peerList.Peers))
for _, peer := range peerList.Peers {
uname, ok := namesById[peer.UserId]
if !ok {
uname = ""
}
dGroupName, ok2 := dGroupNameById[peer.GroupId]
if !ok2 {
dGroupName = ""
}
pp := &apiResp.GroupPeerPayload{}
pp.FromPeer(peer, uname)
pp.FromPeer(peer, uname, dGroupName)
data = append(data, pp)
}
@@ -111,3 +120,31 @@ func (g *Group) Peers(c *gin.Context) {
Data: data,
})
}
// Device
// @Tags 群组
// @Summary 设备
// @Description 机器
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param pageSize query int false "每页数量"
// @Param status query int false "状态"
// @Param accessible query string false "accessible"
// @Success 200 {object} response.DataResponse
// @Failure 500 {object} response.Response
// @Router /device-group/accessible [get]
// @Security BearerAuth
func (g *Group) Device(c *gin.Context) {
u := service.AllService.UserService.CurUser(c)
if !service.AllService.UserService.IsAdmin(u) {
response.Error(c, "Permission denied")
return
}
allGroup := service.AllService.GroupService.DeviceGroupList(1, 999, nil)
c.JSON(http.StatusOK, response.DataResponse{
Total: 0,
Data: allGroup.DeviceGroups,
})
}

View File

@@ -1,13 +1,12 @@
package api
import (
requstform "Gwen/http/request/api"
"Gwen/http/response"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin"
requstform "github.com/lejianwen/rustdesk-api/v2/http/request/api"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service"
"net/http"
"os"
"time"
)
@@ -56,7 +55,7 @@ func (i *Index) Heartbeat(c *gin.Context) {
return
}
//如果在40s以内则不更新
if time.Now().Unix()-peer.LastOnlineTime > 40 {
if time.Now().Unix()-peer.LastOnlineTime >= 30 {
upp := &model.Peer{RowId: peer.RowId, LastOnlineTime: time.Now().Unix(), LastOnlineIp: c.ClientIP()}
service.AllService.PeerService.Update(upp)
}
@@ -74,13 +73,9 @@ func (i *Index) Heartbeat(c *gin.Context) {
// @Router /version [get]
func (i *Index) Version(c *gin.Context) {
//读取resources/version文件
v, err := os.ReadFile("resources/version")
if err != nil {
response.Fail(c, 101, err.Error())
return
}
v := service.AllService.AppService.GetAppVersion()
response.Success(
c,
string(v),
v,
)
}

View File

@@ -1,15 +1,15 @@
package api
import (
"Gwen/global"
"Gwen/http/request/api"
"Gwen/http/response"
apiResp "Gwen/http/response/api"
"Gwen/model"
"Gwen/service"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/api"
"github.com/lejianwen/rustdesk-api/v2/http/response"
apiResp "github.com/lejianwen/rustdesk-api/v2/http/response/api"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service"
"net/http"
)
@@ -27,10 +27,20 @@ type Login struct {
// @Failure 500 {object} response.ErrorResponse
// @Router /login [post]
func (l *Login) Login(c *gin.Context) {
if global.Config.App.DisablePwdLogin {
response.Error(c, response.TranslateMsg(c, "PwdLoginDisabled"))
return
}
// 检查登录限制
loginLimiter := global.LoginLimiter
clientIp := c.ClientIP()
f := &api.LoginForm{}
err := c.ShouldBindJSON(f)
//fmt.Println(f)
if err != nil {
loginLimiter.RecordFailedAttempt(clientIp)
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "ParamsError", c.RemoteIP(), c.ClientIP()))
response.Error(c, response.TranslateMsg(c, "ParamsError")+err.Error())
return
@@ -38,6 +48,7 @@ func (l *Login) Login(c *gin.Context) {
errList := global.Validator.ValidStruct(c, f)
if len(errList) > 0 {
loginLimiter.RecordFailedAttempt(clientIp)
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "ParamsError", c.RemoteIP(), c.ClientIP()))
response.Error(c, errList[0])
return
@@ -46,11 +57,17 @@ func (l *Login) Login(c *gin.Context) {
u := service.AllService.UserService.InfoByUsernamePassword(f.Username, f.Password)
if u.Id == 0 {
loginLimiter.RecordFailedAttempt(clientIp)
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "UsernameOrPasswordError", c.RemoteIP(), c.ClientIP()))
response.Error(c, response.TranslateMsg(c, "UsernameOrPasswordError"))
return
}
if !service.AllService.UserService.CheckUserEnable(u) {
response.Error(c, response.TranslateMsg(c, "UserDisabled"))
return
}
//根据refer判断是webclient还是app
ref := c.GetHeader("referer")
if ref != "" {
@@ -85,7 +102,9 @@ func (l *Login) Login(c *gin.Context) {
// @Router /login-options [get]
func (l *Login) LoginOptions(c *gin.Context) {
ops := service.AllService.OauthService.GetOauthProviders()
ops = append(ops, model.OauthTypeWebauth)
if global.Config.App.WebSso {
ops = append(ops, model.OauthTypeWebauth)
}
var oidcItems []map[string]string
for _, v := range ops {
oidcItems = append(oidcItems, map[string]string{"name": v})

View File

@@ -1,13 +1,15 @@
package api
import (
"Gwen/global"
"Gwen/http/request/api"
"Gwen/http/response"
apiResp "Gwen/http/response/api"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/api"
"github.com/lejianwen/rustdesk-api/v2/http/response"
apiResp "github.com/lejianwen/rustdesk-api/v2/http/response/api"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service"
"github.com/lejianwen/rustdesk-api/v2/utils"
"github.com/nicksnyder/go-i18n/v2/i18n"
"net/http"
)
@@ -32,15 +34,14 @@ func (o *Oauth) OidcAuth(c *gin.Context) {
}
oauthService := service.AllService.OauthService
var code string
var url string
err, code, url = oauthService.BeginAuth(f.Op)
err, state, verifier, nonce, url := oauthService.BeginAuth(f.Op)
if err != nil {
response.Error(c, response.TranslateMsg(c, err.Error()))
return
}
service.AllService.OauthService.SetOauthCache(code, &service.OauthCacheItem{
service.AllService.OauthService.SetOauthCache(state, &service.OauthCacheItem{
Action: service.OauthActionTypeLogin,
Id: f.Id,
Op: f.Op,
@@ -48,10 +49,12 @@ func (o *Oauth) OidcAuth(c *gin.Context) {
DeviceName: f.DeviceInfo.Name,
DeviceOs: f.DeviceInfo.Os,
DeviceType: f.DeviceInfo.Type,
Verifier: verifier,
Nonce: nonce,
}, 5*60)
//fmt.Println("code url", code, url)
c.JSON(http.StatusOK, gin.H{
"code": code,
"code": state,
"url": url,
})
}
@@ -143,7 +146,10 @@ func (o *Oauth) OidcAuthQuery(c *gin.Context) {
func (o *Oauth) OauthCallback(c *gin.Context) {
state := c.Query("state")
if state == "" {
c.String(http.StatusInternalServerError, response.TranslateParamMsg(c, "ParamIsEmpty", "state"))
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
"message": "ParamIsEmpty",
"sub_message": "state",
})
return
}
cacheKey := state
@@ -151,17 +157,24 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
//从缓存中获取
oauthCache := oauthService.GetOauthCache(cacheKey)
if oauthCache == nil {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthExpired"))
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
"message": "OauthExpired",
})
return
}
nonce := oauthCache.Nonce
op := oauthCache.Op
action := oauthCache.Action
verifier := oauthCache.Verifier
var user *model.User
// 获取用户信息
code := c.Query("code")
err, oauthUser := oauthService.Callback(code, op)
err, oauthUser := oauthService.Callback(code, verifier, op, nonce)
if err != nil {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthFailed")+response.TranslateMsg(c, err.Error()))
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
"message": "OauthFailed",
"sub_message": err.Error(),
})
return
}
userId := oauthCache.UserId
@@ -172,28 +185,38 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
// 检查此openid是否已经绑定过
utr := oauthService.UserThirdInfo(op, openid)
if utr.UserId > 0 {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthHasBindOtherUser"))
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
"message": "OauthHasBindOtherUser",
})
return
}
//绑定
user = service.AllService.UserService.InfoById(userId)
if user == nil {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "ItemNotFound"))
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
"message": "ItemNotFound",
})
return
}
//绑定
err := oauthService.BindOauthUser(userId, oauthUser, op)
if err != nil {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "BindFail"))
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
"message": "BindFail",
})
return
}
c.String(http.StatusOK, response.TranslateMsg(c, "BindSuccess"))
c.HTML(http.StatusOK, "oauth_success.html", gin.H{
"message": "BindSuccess",
})
return
} else if action == service.OauthActionTypeLogin {
//登录
if userId != 0 {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthHasBeenSuccess"))
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
"message": "OauthHasBeenSuccess",
})
return
}
user = service.AllService.UserService.InfoByOauthId(op, openid)
@@ -210,7 +233,9 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
//自动注册
err, user = service.AllService.UserService.RegisterByOauth(oauthUser, op)
if err != nil {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, err.Error()))
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
"message": err.Error(),
})
return
}
}
@@ -230,11 +255,51 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
c.Redirect(http.StatusFound, url)
return
}
c.String(http.StatusOK, response.TranslateMsg(c, "OauthSuccess"))
c.HTML(http.StatusOK, "oauth_success.html", gin.H{
"message": "OauthSuccess",
})
return
} else {
c.String(http.StatusInternalServerError, response.TranslateMsg(c, "ParamsError"))
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
"message": "ParamsError",
})
return
}
}
type MessageParams struct {
Lang string `json:"lang" form:"lang"`
Title string `json:"title" form:"title"`
Msg string `json:"msg" form:"msg"`
}
func (o *Oauth) Message(c *gin.Context) {
mp := &MessageParams{}
if err := c.ShouldBindQuery(mp); err != nil {
return
}
localizer := global.Localizer(mp.Lang)
res := ""
if mp.Title != "" {
title, err := localizer.LocalizeMessage(&i18n.Message{
ID: mp.Title,
})
if err == nil {
res = utils.StringConcat(";title='", title, "';")
}
}
if mp.Msg != "" {
msg, err := localizer.LocalizeMessage(&i18n.Message{
ID: mp.Msg,
})
if err == nil {
res = utils.StringConcat(res, "msg = '", msg, "';")
}
}
//返回js内容
c.Header("Content-Type", "application/javascript")
c.String(http.StatusOK, res)
}

View File

@@ -1,11 +1,12 @@
package api
import (
requstform "Gwen/http/request/api"
"Gwen/http/response"
"Gwen/service"
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
requstform "github.com/lejianwen/rustdesk-api/v2/http/request/api"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/service"
"net/http"
)
@@ -13,7 +14,7 @@ type Peer struct {
}
// SysInfo
// @Tags 地址
// @Tags System
// @Summary 提交系统信息
// @Description 提交系统信息
// @Accept json
@@ -30,7 +31,7 @@ func (p *Peer) SysInfo(c *gin.Context) {
return
}
fpe := f.ToPeer()
pe := service.AllService.PeerService.FindById(f.Id)
pe := service.AllService.PeerService.FindByUuid(f.Uuid)
if pe.RowId == 0 {
pe = f.ToPeer()
pe.UserId = service.AllService.UserService.FindLatestUserIdFromLoginLogByUuid(pe.Uuid)
@@ -56,3 +57,20 @@ func (p *Peer) SysInfo(c *gin.Context) {
//直接响应文本
c.String(http.StatusOK, "SYSINFO_UPDATED")
}
// SysInfoVer
// @Tags System
// @Summary 获取系统版本信息
// @Description 获取系统版本信息
// @Accept json
// @Produce json
// @Success 200 {string} string ""
// @Failure 500 {object} response.ErrorResponse
// @Router /sysinfo_ver [post]
func (p *Peer) SysInfoVer(c *gin.Context) {
//读取resources/version文件
v := service.AllService.AppService.GetAppVersion()
// 加上启动时间方便client上传信息
v = fmt.Sprintf("%s\n%s", v, service.AllService.AppService.GetStartTime())
c.String(http.StatusOK, v)
}

View File

@@ -1,9 +1,9 @@
package api
import (
apiResp "Gwen/http/response/api"
"Gwen/service"
"github.com/gin-gonic/gin"
apiResp "github.com/lejianwen/rustdesk-api/v2/http/response/api"
"github.com/lejianwen/rustdesk-api/v2/service"
"net/http"
)

View File

@@ -1,11 +1,11 @@
package api
import (
"Gwen/global"
"Gwen/http/response"
"Gwen/http/response/api"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/http/response/api"
"github.com/lejianwen/rustdesk-api/v2/service"
"time"
)

View File

@@ -1,9 +1,9 @@
package web
import (
"Gwen/global"
"fmt"
"github.com/gin-gonic/gin"
"strconv"
"github.com/lejianwen/rustdesk-api/v2/global"
)
type Index struct {
@@ -15,13 +15,21 @@ func (i *Index) Index(c *gin.Context) {
func (i *Index) ConfigJs(c *gin.Context) {
apiServer := global.Config.Rustdesk.ApiServer
magicQueryonline := strconv.Itoa(global.Config.Rustdesk.WebclientMagicQueryonline)
tmp := `
localStorage.setItem('api-server', "` + apiServer + `")
const ws2_prefix = 'wc-'
localStorage.setItem(ws2_prefix+'api-server', "` + apiServer + `")
magicQueryonline := global.Config.Rustdesk.WebclientMagicQueryonline
tmp := fmt.Sprintf(`localStorage.setItem('api-server', '%v');
const ws2_prefix = 'wc-';
localStorage.setItem(ws2_prefix+'api-server', '%v');
window.webclient_magic_queryonline = ` + magicQueryonline + ``
window.webclient_magic_queryonline = %d;
window.ws_host = '%v';
`, apiServer, apiServer, magicQueryonline, global.Config.Rustdesk.WsHost)
// tmp := `
//localStorage.setItem('api-server', "` + apiServer + `")
//const ws2_prefix = 'wc-'
//localStorage.setItem(ws2_prefix+'api-server', "` + apiServer + `")
//
//window.webclient_magic_queryonline = ` + magicQueryonline + ``
c.Header("Content-Type", "application/javascript")
c.String(200, tmp)
}

View File

@@ -1,10 +1,10 @@
package http
import (
"Gwen/global"
"Gwen/http/middleware"
"Gwen/http/router"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/middleware"
"github.com/lejianwen/rustdesk-api/v2/http/router"
"github.com/sirupsen/logrus"
"net/http"
"strings"
@@ -33,7 +33,7 @@ func ApiInit() {
g.NoRoute(func(c *gin.Context) {
c.String(http.StatusNotFound, "404 not found")
})
g.Use(middleware.Logger(), gin.Recovery())
g.Use(middleware.Logger(), middleware.Limiter(), gin.Recovery())
router.WebInit(g)
router.Init(g)
router.ApiInit(g)

View File

@@ -1,13 +1,13 @@
package middleware
import (
"Gwen/http/response"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/service"
)
// AdminAuth 后台权限验证中间件
func AdminAuth() gin.HandlerFunc {
// BackendUserAuth 后台权限验证中间件
func BackendUserAuth() gin.HandlerFunc {
return func(c *gin.Context) {
//测试先关闭
@@ -24,6 +24,14 @@ func AdminAuth() gin.HandlerFunc {
return
}
if !service.AllService.UserService.CheckUserEnable(user) {
c.JSON(401, gin.H{
"error": "Unauthorized",
})
c.Abort()
return
}
c.Set("curUser", user)
c.Set("token", token)
//如果时间小于1天,token自动续期

View File

@@ -1,9 +1,9 @@
package middleware
import (
"Gwen/http/response"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/service"
)
// AdminPrivilege ...

View File

@@ -1,10 +1,10 @@
package middleware
import (
"Gwen/global"
"Gwen/http/response"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/service"
)
func JwtAuth() gin.HandlerFunc {

View File

@@ -0,0 +1,22 @@
package middleware
import (
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/response"
"net/http"
)
func Limiter() gin.HandlerFunc {
return func(c *gin.Context) {
loginLimiter := global.LoginLimiter
clientIp := c.ClientIP()
banned, _ := loginLimiter.CheckSecurityStatus(clientIp)
if banned {
response.Fail(c, http.StatusLocked, response.TranslateMsg(c, "Banned"))
c.Abort()
return
}
c.Next()
}
}

View File

@@ -1,8 +1,8 @@
package middleware
import (
"Gwen/global"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/sirupsen/logrus"
)

View File

@@ -1,9 +1,9 @@
package middleware
import (
"Gwen/global"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/service"
)
func RustAuth() gin.HandlerFunc {

View File

@@ -1,8 +1,8 @@
package admin
import (
"Gwen/model"
"encoding/json"
"github.com/lejianwen/rustdesk-api/v2/model"
)
type AddressBookForm struct {

View File

@@ -1,6 +1,6 @@
package admin
import "Gwen/model"
import "github.com/lejianwen/rustdesk-api/v2/model"
type GroupForm struct {
Id uint `json:"id"`
@@ -22,3 +22,15 @@ func (gf *GroupForm) ToGroup() *model.Group {
group.Type = gf.Type
return group
}
type DeviceGroupForm struct {
Id uint `json:"id"`
Name string `json:"name" validate:"required"`
}
func (gf *DeviceGroupForm) ToDeviceGroup() *model.DeviceGroup {
group := &model.DeviceGroup{}
group.Id = gf.Id
group.Name = gf.Name
return group
}

View File

@@ -1,10 +1,11 @@
package admin
type Login struct {
Username string `json:"username" validate:"required" label:"用户名"`
Password string `json:"password,omitempty" validate:"required" label:"密码"`
Platform string `json:"platform" label:"平台"`
Captcha string `json:"captcha,omitempty" label:"验证码"`
Username string `json:"username" validate:"required" label:"用户名"`
Password string `json:"password,omitempty" validate:"required" label:"密码"`
Platform string `json:"platform" label:"平台"`
Captcha string `json:"captcha,omitempty" label:"验证码"`
CaptchaId string `json:"captcha_id,omitempty"`
}
type LoginLogQuery struct {

View File

@@ -1,7 +1,7 @@
package admin
import (
"Gwen/model"
"github.com/lejianwen/rustdesk-api/v2/model"
)
type BindOauthForm struct {
@@ -15,27 +15,31 @@ type UnBindOauthForm struct {
Op string `json:"op" binding:"required"`
}
type OauthForm struct {
Id uint `json:"id"`
Op string `json:"op" validate:"omitempty"`
OauthType string `json:"oauth_type" validate:"required"`
Issuer string `json:"issuer" validate:"omitempty,url"`
Scopes string `json:"scopes" validate:"omitempty"`
ClientId string `json:"client_id" validate:"required"`
ClientSecret string `json:"client_secret" validate:"required"`
RedirectUrl string `json:"redirect_url" validate:"required"`
AutoRegister *bool `json:"auto_register"`
Id uint `json:"id"`
Op string `json:"op" validate:"omitempty"`
OauthType string `json:"oauth_type" validate:"required"`
Issuer string `json:"issuer" validate:"omitempty,url"`
Scopes string `json:"scopes" validate:"omitempty"`
ClientId string `json:"client_id" validate:"required"`
ClientSecret string `json:"client_secret" validate:"required"`
RedirectUrl string `json:"redirect_url" validate:"required"`
AutoRegister *bool `json:"auto_register"`
PkceEnable *bool `json:"pkce_enable"`
PkceMethod string `json:"pkce_method"`
}
func (of *OauthForm) ToOauth() *model.Oauth {
oa := &model.Oauth{
Op: of.Op,
OauthType: of.OauthType,
OauthType: of.OauthType,
ClientId: of.ClientId,
ClientSecret: of.ClientSecret,
RedirectUrl: of.RedirectUrl,
AutoRegister: of.AutoRegister,
Issuer: of.Issuer,
Scopes: of.Scopes,
PkceEnable: of.PkceEnable,
PkceMethod: of.PkceMethod,
}
oa.Id = of.Id
return oa

View File

@@ -1,6 +1,6 @@
package admin
import "Gwen/model"
import "github.com/lejianwen/rustdesk-api/v2/model"
type PeerForm struct {
RowId uint `json:"row_id" `
@@ -12,6 +12,7 @@ type PeerForm struct {
Username string `json:"username"`
Uuid string `json:"uuid"`
Version string `json:"version"`
GroupId uint `json:"group_id"`
}
type PeerBatchDeleteForm struct {
@@ -30,6 +31,7 @@ func (f *PeerForm) ToPeer() *model.Peer {
Username: f.Username,
Uuid: f.Uuid,
Version: f.Version,
GroupId: f.GroupId,
}
}
@@ -39,6 +41,8 @@ type PeerQuery struct {
Id string `json:"id" form:"id"`
Hostname string `json:"hostname" form:"hostname"`
Uuids string `json:"uuids" form:"uuids"`
Ip string `json:"ip" form:"ip"`
Username string `json:"username" form:"username"`
}
type SimpleDataQuery struct {

View File

@@ -1,6 +1,6 @@
package admin
import "Gwen/model"
import "github.com/lejianwen/rustdesk-api/v2/model"
type TagForm struct {
Id uint `json:"id"`

View File

@@ -1,7 +1,7 @@
package admin
import (
"Gwen/model"
"github.com/lejianwen/rustdesk-api/v2/model"
)
type UserForm struct {

View File

@@ -1,9 +1,9 @@
package api
import (
"Gwen/global"
"Gwen/model"
"encoding/json"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/model"
"strconv"
)

View File

@@ -1,6 +1,6 @@
package api
import "Gwen/model"
import "github.com/lejianwen/rustdesk-api/v2/model"
type AddressBookFormData struct {
Tags []string `json:"tags"`

View File

@@ -40,14 +40,14 @@ type LoginForm struct {
type UserListQuery struct {
Page uint `json:"page" form:"page" validate:"required" label:"页码"`
PageSize uint `json:"page_size" form:"page_size" validate:"required" label:"每页数量"`
PageSize uint `json:"pageSize" form:"pageSize" validate:"required" label:"每页数量"`
Status int `json:"status" form:"status" label:"状态"`
Accessible string `json:"accessible" form:"accessible"`
}
type PeerListQuery struct {
Page uint `json:"page" form:"page" validate:"required" label:"页码"`
PageSize uint `json:"page_size" form:"page_size" validate:"required" label:"每页数量"`
PageSize uint `json:"pageSize" form:"pageSize" validate:"required" label:"每页数量"`
Status int `json:"status" form:"status" label:"状态"`
Accessible string `json:"accessible" form:"accessible"`
}

View File

@@ -1,6 +1,6 @@
package admin
import "Gwen/model"
import "github.com/lejianwen/rustdesk-api/v2/model"
type LoginPayload struct {
Username string `json:"username"`
@@ -22,15 +22,3 @@ type UserOauthItem struct {
Op string `json:"op"`
Status int `json:"status"`
}
type GroupUsersPayload struct {
Id uint `json:"id"`
Username string `json:"username"`
Status int `json:"status"`
}
func (g *GroupUsersPayload) FromUser(user *model.User) {
g.Id = user.Id
g.Username = user.Username
g.Status = 1
}

View File

@@ -1,6 +1,6 @@
package api
import "Gwen/model"
import "github.com/lejianwen/rustdesk-api/v2/model"
type AbList struct {
Peers []*model.AddressBook `json:"peers,omitempty"`

View File

@@ -1,6 +1,6 @@
package api
import "Gwen/model"
import "github.com/lejianwen/rustdesk-api/v2/model"
/*
GroupPeerPayload
@@ -32,12 +32,13 @@ https://github.com/rustdesk/rustdesk/blob/master/flutter/lib/common/hbbs/hbbs.da
}
*/
type GroupPeerPayload struct {
Id string `json:"id"`
Info *PeerPayloadInfo `json:"info"`
Status int `json:"status"`
User string `json:"user"`
UserName string `json:"user_name"`
Note string `json:"note"`
Id string `json:"id"`
Info *PeerPayloadInfo `json:"info"`
Status int `json:"status"`
User string `json:"user"`
UserName string `json:"user_name"`
Note string `json:"note"`
DeviceGroupName string `json:"device_group_name"`
}
type PeerPayloadInfo struct {
DeviceName string `json:"device_name"`
@@ -59,7 +60,7 @@ func (gpp *GroupPeerPayload) FromAddressBook(a *model.AddressBook, username stri
gpp.UserName = username
}
func (gpp *GroupPeerPayload) FromPeer(p *model.Peer, username string) {
func (gpp *GroupPeerPayload) FromPeer(p *model.Peer, username string, dGroupName string) {
gpp.Id = p.Id
gpp.Info = &PeerPayloadInfo{
DeviceName: p.Hostname,
@@ -68,4 +69,5 @@ func (gpp *GroupPeerPayload) FromPeer(p *model.Peer, username string) {
}
gpp.Note = ""
gpp.UserName = username
gpp.DeviceGroupName = dGroupName
}

View File

@@ -1,6 +1,6 @@
package api
import "Gwen/model"
import "github.com/lejianwen/rustdesk-api/v2/model"
/*
pub enum UserStatus {

View File

@@ -1,7 +1,7 @@
package api
import (
"Gwen/model"
"github.com/lejianwen/rustdesk-api/v2/model"
"time"
)

View File

@@ -1,9 +1,9 @@
package response
import (
"Gwen/global"
"fmt"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/nicksnyder/go-i18n/v2/i18n"
"net/http"
)

View File

@@ -1,12 +1,12 @@
package router
import (
_ "Gwen/docs/admin"
"Gwen/global"
"Gwen/http/controller/admin"
"Gwen/http/controller/admin/my"
"Gwen/http/middleware"
"github.com/gin-gonic/gin"
_ "github.com/lejianwen/rustdesk-api/v2/docs/admin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/controller/admin"
"github.com/lejianwen/rustdesk-api/v2/http/controller/admin/my"
"github.com/lejianwen/rustdesk-api/v2/http/middleware"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
@@ -22,7 +22,10 @@ func Init(g *gin.Engine) {
adg := g.Group("/api/admin")
LoginBind(adg)
adg.POST("/user/register", (&admin.User{}).Register)
adg.Use(middleware.AdminAuth())
ConfigBind(adg)
adg.Use(middleware.BackendUserAuth())
//FileBind(adg)
UserBind(adg)
GroupBind(adg)
@@ -35,7 +38,6 @@ func Init(g *gin.Engine) {
AddressBookCollectionBind(adg)
AddressBookCollectionRuleBind(adg)
UserTokenBind(adg)
ConfigBind(adg)
//deprecated by ConfigBind
//rs := &admin.Rustdesk{}
@@ -47,7 +49,7 @@ func Init(g *gin.Engine) {
MyBind(adg)
RustdeskCmdBind(adg)
DeviceGroupBind(adg)
//访问静态文件
//g.StaticFS("/upload", http.Dir(global.Config.Gin.ResourcesPath+"/upload"))
}
@@ -104,6 +106,18 @@ func GroupBind(rg *gin.RouterGroup) {
}
}
func DeviceGroupBind(rg *gin.RouterGroup) {
aR := rg.Group("/device_group").Use(middleware.AdminPrivilege())
{
cont := &admin.DeviceGroup{}
aR.GET("/list", cont.List)
aR.GET("/detail/:id", cont.Detail)
aR.POST("/create", cont.Create)
aR.POST("/update", cont.Update)
aR.POST("/delete", cont.Delete)
}
}
func TagBind(rg *gin.RouterGroup) {
aR := rg.Group("/tag").Use(middleware.AdminPrivilege())
{
@@ -221,9 +235,13 @@ func UserTokenBind(rg *gin.RouterGroup) {
func ConfigBind(rg *gin.RouterGroup) {
aR := rg.Group("/config")
rs := &admin.Config{}
aR.GET("/admin", rs.AdminConfig)
aR.Use(middleware.BackendUserAuth())
aR.GET("/server", rs.ServerConfig)
aR.GET("/app", rs.AppConfig)
aR.GET("/admin", rs.AdminConfig)
}
/*

View File

@@ -1,11 +1,11 @@
package router
import (
_ "Gwen/docs/api"
"Gwen/global"
"Gwen/http/controller/api"
"Gwen/http/middleware"
"github.com/gin-gonic/gin"
_ "github.com/lejianwen/rustdesk-api/v2/docs/api"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/controller/api"
"github.com/lejianwen/rustdesk-api/v2/http/middleware"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"net/http"
@@ -18,6 +18,8 @@ func ApiInit(g *gin.Engine) {
if global.Config.App.ShowSwagger == 1 {
g.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, ginSwagger.InstanceName("api")))
}
// 加载 HTML 模板
g.LoadHTMLGlob("resources/templates/*")
frg := g.Group("/api")
@@ -46,11 +48,13 @@ func ApiInit(g *gin.Engine) {
//api/oauth/callback
frg.GET("/oauth/callback", o.OauthCallback)
frg.GET("/oauth/login", o.OauthCallback)
frg.GET("/oauth/msg", o.Message)
}
{
pe := &api.Peer{}
//提交系统信息
frg.POST("/sysinfo", pe.SysInfo)
frg.POST("/sysinfo_ver", pe.SysInfoVer)
}
if global.Config.App.WebClient == 1 {
@@ -79,6 +83,8 @@ func ApiInit(g *gin.Engine) {
gr := &api.Group{}
frg.GET("/users", gr.Users)
frg.GET("/peers", gr.Peers)
// /api/device-group/accessible?current=1&pageSize=100
frg.GET("/device-group/accessible", gr.Device)
}
{
@@ -88,6 +94,7 @@ func ApiInit(g *gin.Engine) {
//更新地址
frg.POST("/ab", ab.UpAb)
}
PersonalRoutes(frg)
//访问静态文件
g.StaticFS("/upload", http.Dir(global.Config.Gin.ResourcesPath+"/public/upload"))

View File

@@ -1,9 +1,9 @@
package router
import (
"Gwen/global"
"Gwen/http/controller/web"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/controller/web"
"net/http"
)

View File

@@ -21,7 +21,7 @@ type Config struct {
func New(c *Config) *log.Logger {
log.SetFormatter(&nested.Formatter{
// HideKeys: true,
TimestampFormat: "2006-01-02 15:04:05",
TimestampFormat: "[2006-01-02 15:04:05]",
NoColors: true,
NoFieldsColors: true,
//FieldsOrder: []string{"name", "age"},

View File

@@ -1,7 +1,6 @@
package orm
import (
"Gwen/global"
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
@@ -10,14 +9,14 @@ import (
)
type MysqlConfig struct {
Dns string
Dsn string
MaxIdleConns int
MaxOpenConns int
}
func NewMysql(mysqlConf *MysqlConfig) *gorm.DB {
func NewMysql(mysqlConf *MysqlConfig, logwriter logger.Writer) *gorm.DB {
db, err := gorm.Open(mysql.New(mysql.Config{
DSN: mysqlConf.Dns, // DSN data source name
DSN: mysqlConf.Dsn, // DSN data source name
DefaultStringSize: 256, // string 类型字段的默认长度
//DisableDatetimePrecision: true, // 禁用 datetime 精度MySQL 5.6 之前的数据库不支持
//DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
@@ -26,7 +25,7 @@ func NewMysql(mysqlConf *MysqlConfig) *gorm.DB {
}), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
Logger: logger.New(
global.Logger, // io writer
logwriter, // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Warn, // Log level

45
lib/orm/postgresql.go Normal file
View File

@@ -0,0 +1,45 @@
package orm
import (
"fmt"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"time"
)
type PostgresqlConfig struct {
Dsn string
MaxIdleConns int
MaxOpenConns int
}
func NewPostgresql(conf *PostgresqlConfig, logwriter logger.Writer) *gorm.DB {
db, err := gorm.Open(postgres.Open(conf.Dsn), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
Logger: logger.New(
logwriter, // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Warn, // Log level
//IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
ParameterizedQueries: true, // Don't include params in the SQL log
Colorful: true,
},
),
})
if err != nil {
fmt.Println(err)
}
sqlDB, err2 := db.DB()
if err2 != nil {
fmt.Println(err2)
}
// SetMaxIdleConns 设置空闲连接池中连接的最大数量
sqlDB.SetMaxIdleConns(conf.MaxIdleConns)
// SetMaxOpenConns 设置打开数据库连接的最大数量。
sqlDB.SetMaxOpenConns(conf.MaxOpenConns)
return db
}

View File

@@ -1,7 +1,6 @@
package orm
import (
"Gwen/global"
"fmt"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
@@ -14,11 +13,11 @@ type SqliteConfig struct {
MaxOpenConns int
}
func NewSqlite(sqliteConf *SqliteConfig) *gorm.DB {
func NewSqlite(sqliteConf *SqliteConfig, logwriter logger.Writer) *gorm.DB {
db, err := gorm.Open(sqlite.Open("./data/rustdeskapi.db"), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
Logger: logger.New(
global.Logger, // io writer
logwriter, // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Warn, // Log level

View File

@@ -1,6 +1,6 @@
package model
import "Gwen/model/custom_types"
import "github.com/lejianwen/rustdesk-api/v2/model/custom_types"
// final String id;
// String hash; // personal ab hash password

View File

@@ -16,3 +16,14 @@ type GroupList struct {
Groups []*Group `json:"list"`
Pagination
}
type DeviceGroup struct {
IdModel
Name string `json:"name" gorm:"default:'';not null;"`
TimeModel
}
type DeviceGroupList struct {
DeviceGroups []*DeviceGroup `json:"list"`
Pagination
}

View File

@@ -1,7 +1,7 @@
package model
import (
"Gwen/model/custom_types"
"github.com/lejianwen/rustdesk-api/v2/model/custom_types"
)
type StatusCode int

View File

@@ -14,12 +14,15 @@ const (
OauthTypeGoogle string = "google"
OauthTypeOidc string = "oidc"
OauthTypeWebauth string = "webauth"
OauthTypeLinuxdo string = "linuxdo"
PKCEMethodS256 string = "S256"
PKCEMethodPlain string = "plain"
)
// Validate the oauth type
func ValidateOauthType(oauthType string) error {
switch oauthType {
case OauthTypeGithub, OauthTypeGoogle, OauthTypeOidc, OauthTypeWebauth:
case OauthTypeGithub, OauthTypeGoogle, OauthTypeOidc, OauthTypeWebauth, OauthTypeLinuxdo:
return nil
default:
return errors.New("invalid Oauth type")
@@ -28,6 +31,7 @@ func ValidateOauthType(oauthType string) error {
const (
UserEndpointGithub string = "https://api.github.com/user"
UserEndpointLinuxdo string = "https://connect.linux.do/api/user"
IssuerGoogle string = "https://accounts.google.com"
)
@@ -41,6 +45,8 @@ type Oauth struct {
AutoRegister *bool `json:"auto_register"`
Scopes string `json:"scopes"`
Issuer string `json:"issuer"`
PkceEnable *bool `json:"pkce_enable"`
PkceMethod string `json:"pkce_method"`
TimeModel
}
@@ -56,6 +62,8 @@ func (oa *Oauth) FormatOauthInfo() error {
oa.Op = OauthTypeGithub
case OauthTypeGoogle:
oa.Op = OauthTypeGoogle
case OauthTypeLinuxdo:
oa.Op = OauthTypeLinuxdo
}
// check if the op is empty, set the default value
op := strings.TrimSpace(oa.Op)
@@ -68,6 +76,13 @@ func (oa *Oauth) FormatOauthInfo() error {
if oauthType == OauthTypeGoogle && issuer == "" {
oa.Issuer = IssuerGoogle
}
if oa.PkceEnable == nil {
oa.PkceEnable = new(bool)
*oa.PkceEnable = false
}
if oa.PkceMethod == "" {
oa.PkceMethod = PKCEMethodS256
}
return nil
}
@@ -141,6 +156,24 @@ func (gu *GithubUser) ToOauthUser() *OauthUser {
}
}
type LinuxdoUser struct {
OauthUserBase
Id int `json:"id"`
Username string `json:"username"`
Avatar string `json:"avatar_url"`
}
func (lu *LinuxdoUser) ToOauthUser() *OauthUser {
return &OauthUser{
OpenId: strconv.Itoa(lu.Id),
Name: lu.Name,
Username: strings.ToLower(lu.Username),
Email: lu.Email,
VerifiedEmail: true, // linux.do 用户邮箱默认已验证
Picture: lu.Avatar,
}
}
type OauthList struct {
Oauths []*Oauth `json:"list"`
Pagination

View File

@@ -14,6 +14,7 @@ type Peer struct {
User *User `json:"user,omitempty"`
LastOnlineTime int64 `json:"last_online_time" gorm:"default:0;not null;"`
LastOnlineIp string `json:"last_online_ip" gorm:"default:'';not null;"`
GroupId uint `json:"group_id" gorm:"default:0;not null;index"`
TimeModel
}

View File

@@ -2,11 +2,11 @@ package model
type UserToken struct {
IdModel
UserId uint `json:"user_id" gorm:"default:0;not null;index"`
DeviceUuid string `json:"device_uuid" gorm:"default:'';omitempty;"`
DeviceId string `json:"device_id" gorm:"default:'';omitempty;"`
Token string `json:"token" gorm:"default:'';not null;index"`
ExpiredAt int64 `json:"expired_at" gorm:"default:0;not null;"`
UserId uint `json:"user_id" gorm:"default:0;not null;index"`
DeviceUuid string `json:"device_uuid" gorm:"default:'';omitempty;"`
DeviceId string `json:"device_id" gorm:"default:'';omitempty;"`
Token string `json:"token" gorm:"default:'';not null;index"`
ExpiredAt int64 `json:"expired_at" gorm:"default:0;not null;"`
TimeModel
}

View File

@@ -133,3 +133,23 @@ other = "Captcha required."
description = "Captcha error."
one = "Captcha error."
other = "Captcha error."
[PwdLoginDisabled]
description = "Password login disabled."
one = "Password login disabled."
other = "Password login disabled."
[CannotShareToSelf]
description = "Cannot share to self."
one = "Cannot share to self."
other = "Cannot share to self."
[Banned]
description = "Banned."
one = "Banned."
other = "Banned."
[RegisterSuccessWaitAdminConfirm]
description = "Register success, wait admin confirm."
one = "Register success, wait admin confirm."
other = "Register success, wait admin confirm."

View File

@@ -141,4 +141,24 @@ other = "Captcha requerido."
[CaptchaError]
description = "Captcha error."
one = "Error de captcha."
other = "Error de captcha."
other = "Error de captcha."
[PwdLoginDisabled]
description = "Password login disabled."
one = "Inicio de sesión con contraseña deshabilitado."
other = "Inicio de sesión con contraseña deshabilitado."
[CannotShareToSelf]
description = "Cannot share to self."
one = "No se puede compartir con uno mismo."
other = "No se puede compartir con uno mismo."
[Banned]
description = "Banned."
one = "Prohibido."
other = "Prohibido."
[RegisterSuccessWaitAdminConfirm]
description = "Register success, wait admin confirm."
one = "Registro exitoso, espere la confirmación del administrador."
other = "Registro exitoso, espere la confirmación del administrador."

View File

@@ -142,3 +142,23 @@ other = "Captcha requis."
description = "Captcha error."
one = "Erreur de captcha."
other = "Erreur de captcha."
[PwdLoginDisabled]
description = "Password login disabled."
one = "Connexion par mot de passe désactivée."
other = "Connexion par mot de passe désactivée."
[CannotShareToSelf]
description = "Cannot share to self."
one = "Impossible de partager avec soi-même."
other = "Impossible de partager avec soi-même."
[Banned]
description = "Banned."
one = "Banni."
other = "Banni."
[RegisterSuccessWaitAdminConfirm]
description = "Register success wait admin confirm."
one = "Inscription réussie, veuillez attendre la confirmation de l'administrateur."
other = "Inscription réussie, veuillez attendre la confirmation de l'administrateur."

Some files were not shown because too many files have changed in this diff Show More