Compare commits

...

37 Commits

Author SHA1 Message Date
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
75 changed files with 94144 additions and 86733 deletions

View File

@@ -380,7 +380,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,
@@ -390,7 +390,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,
@@ -401,7 +401,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,
@@ -411,7 +411,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,
@@ -422,7 +422,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,
@@ -433,7 +433,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

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

View File

@@ -9,7 +9,7 @@
<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://img.shields.io/badge/i18n-7-green"/>
<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>
@@ -109,7 +109,6 @@
* 可以添加自定义指令
* 可以执行自定义指令
![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验证失败返回本地用户
@@ -146,69 +145,11 @@
### 相关配置
* [配置文件](./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
ldap:
enable: false
url: "ldap://ldap.example.com:389"
tls: false
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.
```
### 环境变量
环境变量和配置文件`conf/config.yaml`中的配置一一对应,变量名前缀是`RUSTDESK_API`
下面表格并未全部列出,可以参考`conf/config.yaml`中的配置。
@@ -220,7 +161,8 @@ ldap:
| 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` |
| RUSTDESK_API_APP_TOKEN_EXPIRE | token有效时长 | `168h` |
| RUSTDESK_API_APP_DISABLE_PWD_LOGIN | 是否禁用密码登录; `true`, `false` 默认`false` | `false` |
| -----ADMIN配置----- | ---------- | ---------- |
| RUSTDESK_API_ADMIN_TITLE | 后台标题 | `RustDesk Api Admin` |
| RUSTDESK_API_ADMIN_HELLO | 后台欢迎语,可以使用`html` | |
@@ -244,12 +186,13 @@ ldap:
| 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有效时间 | 360000 |
| RUSTDESK_API_JWT_EXPIRE_DURATION | JWT有效时间 | `168h` |
### 运行

View File

@@ -8,7 +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://img.shields.io/badge/i18n-7-green"/>
<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>
@@ -109,8 +109,6 @@ displaying data.Frontend code is available at [rustdesk-api-web](https://github.
* 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:
@@ -145,81 +143,25 @@ displaying data.Frontend code is available at [rustdesk-api-web](https://github.
### 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
ldap:
enable: false
url: "ldap://ldap.example.com:389"
tls: false
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.
```
### Environment Variables
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` |
| RUSTDESK_API_APP_TOKEN_EXPIRE | token expire duration | `168h` |
| RUSTDESK_API_APP_DISABLE_PWD_LOGIN | disable password login | `false` |
| ----- ADMIN Configuration----- | ---------- | ---------- |
| RUSTDESK_API_ADMIN_TITLE | Admin Title | `RustDesk Api Admin` |
| RUSTDESK_API_ADMIN_HELLO | Admin welcome message, you can use `html` | |
@@ -243,12 +185,13 @@ The table below does not list all configurations. Please refer to the configurat
| 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 | 360000 |
| RUSTDESK_API_JWT_EXPIRE_DURATION | JWT expire duration | `168h` |
### Installation Steps

View File

@@ -18,7 +18,6 @@ import (
"github.com/spf13/cobra"
"os"
"strconv"
"time"
)
// @title 管理系统API
@@ -52,6 +51,10 @@ var resetPwdCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
pwd := args[0]
admin := service.AllService.UserService.InfoById(1)
if admin.Id == 0 {
global.Logger.Warn("user not found! ")
return
}
err := service.AllService.UserService.UpdatePassword(admin, pwd)
if err != nil {
global.Logger.Error("reset password fail! ", err)
@@ -78,6 +81,10 @@ var resetUserPwdCmd = &cobra.Command{
return
}
u := service.AllService.UserService.InfoById(uint(uid))
if u.Id == 0 {
global.Logger.Warn("user not found! ")
return
}
err = service.AllService.UserService.UpdatePassword(u, pwd)
if err != nil {
global.Logger.Warn("reset password fail! ", err)
@@ -145,7 +152,6 @@ func InitGlobal() {
MaxOpenConns: global.Config.Gorm.MaxOpenConns,
})
}
DatabaseAutoUpdate()
//validator
global.ApiInitValidator()
@@ -162,13 +168,17 @@ 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)
DatabaseAutoUpdate()
}
func DatabaseAutoUpdate() {
version := 260
version := 262
db := global.DB
@@ -212,6 +222,7 @@ func DatabaseAutoUpdate() {
if v.Version < uint(version) {
Migrate(uint(version))
}
// 245迁移
if v.Version < 245 {
//oauths 表的 oauth_type 字段设置为 op同样的值
@@ -234,7 +245,7 @@ func DatabaseAutoUpdate() {
}
func Migrate(version uint) {
global.Logger.Info("migrating....", version)
global.Logger.Info("Migrating....", version)
err := global.DB.AutoMigrate(
&model.Version{},
&model.User{},
@@ -252,6 +263,7 @@ func Migrate(version uint) {
&model.AddressBookCollection{},
&model.AddressBookCollectionRule{},
&model.ServerCmd{},
&model.DeviceGroup{},
)
if err != nil {
global.Logger.Error("migrate err :=>", err)

View File

@@ -3,7 +3,7 @@ app:
web-client: 1 # 1:启用 0:禁用
register: false #是否开启注册
show-swagger: 0 # 1:启用 0:禁用
token-expire: 360000
token-expire: 168h
web-sso: true #web auth sso
disable-pwd-login: false #禁用密码登录
admin:
@@ -32,6 +32,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
@@ -41,11 +42,11 @@ proxy:
host: "http://127.0.0.1:1080"
jwt:
key: ""
expire-duration: 360000
expire-duration: 168h
ldap:
enable: false
url: "ldap://ldap.example.com:389"
tls: false
tls-ca-file: ""
tls-verify: false
base-dn: "dc=example,dc=com"
bind-dn: "cn=admin,dc=example,dc=com"
@@ -63,21 +64,3 @@ ldap:
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.
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

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/spf13/viper"
"strings"
"time"
)
const (
@@ -16,7 +17,7 @@ type App struct {
WebClient int `mapstructure:"web-client"`
Register bool `mapstructure:"register"`
ShowSwagger int `mapstructure:"show-swagger"`
TokenExpire int `mapstructure:"token-expire"`
TokenExpire time.Duration `mapstructure:"token-expire"`
WebSso bool `mapstructure:"web-sso"`
DisablePwdLogin bool `mapstructure:"disable-pwd-login"`
}
@@ -73,7 +74,7 @@ func Init(rowVal *Config, path string) *viper.Viper {
})
*/
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()

View File

@@ -26,7 +26,7 @@ type LdapUser struct {
type Ldap struct {
Enable bool `mapstructure:"enable"`
Url string `mapstructure:"url"`
TLS bool `mapstructure:"tls"`
TlsCaFile string `mapstructure:"tls-ca-file"`
TlsVerify bool `mapstructure:"tls-verify"`
BaseDn string `mapstructure:"base-dn"`
BindDn string `mapstructure:"bind-dn"`

View File

@@ -22,6 +22,7 @@ type Rustdesk struct {
Personal int `mapstructure:"personal"`
//webclient-magic-queryonline
WebclientMagicQueryonline int `mapstructure:"webclient-magic-queryonline"`
WsHost string `mapstructure:"ws-host"`
}
func (rd *Rustdesk) LoadKeyFile() {

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/github_com_lejianwen_rustdesk-api_http_request_admin.Login"
"$ref": "#/definitions/github_com_lejianwen_rustdesk-api_v2_http_request_admin.Login"
}
}
],
@@ -5219,6 +5493,20 @@ const docTemplateadmin = `{
}
}
},
"admin.DeviceGroupForm": {
"type": "object",
"required": [
"name"
],
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
}
}
},
"admin.GroupForm": {
"type": "object",
"required": [
@@ -5306,6 +5594,12 @@ const docTemplateadmin = `{
"op": {
"type": "string"
},
"pkce_enable": {
"type": "boolean"
},
"pkce_method": {
"type": "string"
},
"redirect_url": {
"type": "string"
},
@@ -5334,6 +5628,9 @@ const docTemplateadmin = `{
"cpu": {
"type": "string"
},
"group_id": {
"type": "integer"
},
"hostname": {
"type": "string"
},
@@ -5521,7 +5818,7 @@ const docTemplateadmin = `{
}
}
},
"github_com_lejianwen_rustdesk-api_http_request_admin.Login": {
"github_com_lejianwen_rustdesk-api_v2_http_request_admin.Login": {
"type": "object",
"required": [
"password",
@@ -5842,6 +6139,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 +6287,12 @@ const docTemplateadmin = `{
"op": {
"type": "string"
},
"pkce_enable": {
"type": "boolean"
},
"pkce_method": {
"type": "string"
},
"redirect_url": {
"type": "string"
},
@@ -6013,6 +6333,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/github_com_lejianwen_rustdesk-api_http_request_admin.Login"
"$ref": "#/definitions/github_com_lejianwen_rustdesk-api_v2_http_request_admin.Login"
}
}
],
@@ -5212,6 +5486,20 @@
}
}
},
"admin.DeviceGroupForm": {
"type": "object",
"required": [
"name"
],
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
}
}
},
"admin.GroupForm": {
"type": "object",
"required": [
@@ -5299,6 +5587,12 @@
"op": {
"type": "string"
},
"pkce_enable": {
"type": "boolean"
},
"pkce_method": {
"type": "string"
},
"redirect_url": {
"type": "string"
},
@@ -5327,6 +5621,9 @@
"cpu": {
"type": "string"
},
"group_id": {
"type": "integer"
},
"hostname": {
"type": "string"
},
@@ -5514,7 +5811,7 @@
}
}
},
"github_com_lejianwen_rustdesk-api_http_request_admin.Login": {
"github_com_lejianwen_rustdesk-api_v2_http_request_admin.Login": {
"type": "object",
"required": [
"password",
@@ -5835,6 +6132,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 +6280,12 @@
"op": {
"type": "string"
},
"pkce_enable": {
"type": "boolean"
},
"pkce_method": {
"type": "string"
},
"redirect_url": {
"type": "string"
},
@@ -6006,6 +6326,9 @@
"created_at": {
"type": "string"
},
"group_id": {
"type": "integer"
},
"hostname": {
"type": "string"
},

View File

@@ -77,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:
@@ -130,6 +139,10 @@ definitions:
type: string
op:
type: string
pkce_enable:
type: boolean
pkce_method:
type: string
redirect_url:
type: string
scopes:
@@ -153,6 +166,8 @@ definitions:
properties:
cpu:
type: string
group_id:
type: integer
hostname:
type: string
id:
@@ -278,7 +293,7 @@ definitions:
required:
- ids
type: object
github_com_lejianwen_rustdesk-api_http_request_admin.Login:
github_com_lejianwen_rustdesk-api_v2_http_request_admin.Login:
properties:
captcha:
type: string
@@ -492,6 +507,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 +605,10 @@ definitions:
type: string
op:
type: string
pkce_enable:
type: boolean
pkce_method:
type: string
redirect_url:
type: string
scopes:
@@ -605,6 +635,8 @@ definitions:
type: string
created_at:
type: string
group_id:
type: integer
hostname:
type: string
id:
@@ -1610,6 +1642,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 +2023,7 @@ paths:
name: body
required: true
schema:
$ref: '#/definitions/github_com_lejianwen_rustdesk-api_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": "心跳",

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": "心跳",

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:

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

15
go.mod
View File

@@ -10,7 +10,7 @@ require (
github.com/gin-gonic/gin v1.9.0
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.6.0
@@ -22,7 +22,7 @@ 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.21.0
golang.org/x/text v0.22.0
gorm.io/driver/mysql v1.5.7
gorm.io/driver/sqlite v1.5.6
gorm.io/gorm v1.25.7
@@ -36,9 +36,12 @@ require (
github.com/bytedance/sonic v1.8.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/coreos/go-oidc/v3 v3.12.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 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-ldap/ldap/v3 v3.4.10 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
@@ -54,7 +57,7 @@ require (
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
@@ -73,10 +76,10 @@ 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.31.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/image v0.13.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/ini.v1 v1.63.2 // indirect

View File

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

@@ -4,6 +4,7 @@ import (
"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,22 @@ func (co *Config) AppConfig(c *gin.Context) {
// @Security token
func (co *Config) AdminConfig(c *gin.Context) {
u := service.AllService.UserService.CurUser(c)
if u == nil || u.Id == 0 {
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 != "" {

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

@@ -283,13 +283,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,
@@ -297,10 +297,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

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

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

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

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

View File

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

@@ -7,7 +7,6 @@ import (
"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

@@ -8,6 +8,8 @@ import (
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

@@ -30,7 +30,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 +56,9 @@ func (p *Peer) SysInfo(c *gin.Context) {
//直接响应文本
c.String(http.StatusOK, "SYSINFO_UPDATED")
}
func (p *Peer) SysInfoVer(c *gin.Context) {
//读取resources/version文件
v := service.AllService.AppService.GetAppVersion()
c.String(http.StatusOK, v)
}

View File

@@ -1,9 +1,9 @@
package web
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global"
"strconv"
)
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

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

@@ -24,6 +24,8 @@ type OauthForm struct {
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 {
@@ -36,6 +38,8 @@ func (of *OauthForm) ToOauth() *model.Oauth {
AutoRegister: of.AutoRegister,
Issuer: of.Issuer,
Scopes: of.Scopes,
PkceEnable: of.PkceEnable,
PkceMethod: of.PkceMethod,
}
oa.Id = of.Id
return oa

View File

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

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

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

@@ -38,6 +38,7 @@ type GroupPeerPayload struct {
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

@@ -49,7 +49,7 @@ func Init(g *gin.Engine) {
MyBind(adg)
RustdeskCmdBind(adg)
DeviceGroupBind(adg)
//访问静态文件
//g.StaticFS("/upload", http.Dir(global.Config.Gin.ResourcesPath+"/upload"))
}
@@ -106,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())
{

View File

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

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

@@ -14,6 +14,8 @@ const (
OauthTypeGoogle string = "google"
OauthTypeOidc string = "oidc"
OauthTypeWebauth string = "webauth"
PKCEMethodS256 string = "S256"
PKCEMethodPlain string = "plain"
)
// Validate the oauth type
@@ -41,6 +43,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
}
@@ -68,6 +72,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
}

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

@@ -138,3 +138,8 @@ other = "Captcha error."
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."

View File

@@ -147,3 +147,8 @@ other = "Error de captcha."
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."

View File

@@ -147,3 +147,8 @@ other = "Erreur de captcha."
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."

View File

@@ -141,3 +141,8 @@ other = "Captcha 오류."
description = "Password login disabled."
one = "비밀번호 로그인이 비활성화되었습니다."
other = "비밀번호 로그인이 비활성화되었습니다."
[CannotShareToSelf]
description = "Cannot share to self."
one = "자기 자신에게 공유할 수 없습니다."
other = "자기 자신에게 공유할 수 없습니다."

View File

@@ -147,3 +147,8 @@ other = "Ошибка капчи."
description = "Password login disabled."
one = "Вход по паролю отключен."
other = "Вход по паролю отключен."
[CannotShareToSelf]
description = "Cannot share to self."
one = "Нельзя поделиться с собой."
other = "Нельзя поделиться с собой."

View File

@@ -140,3 +140,8 @@ other = "验证码错误。"
description = "Password login disabled."
one = "密码登录已禁用。"
other = "密码登录已禁用。"
[CannotShareToSelf]
description = "Cannot share to self."
one = "不能共享给自己。"
other = "不能共享给自己。"

View File

@@ -140,3 +140,8 @@ other = "驗證碼錯誤。"
description = "Password login disabled."
one = "密碼登錄已禁用。"
other = "密碼登錄已禁用。"
[CannotShareToSelf]
description = "Cannot share to self."
one = "無法共享給自己。"
other = "無法共享給自己。"

View File

View File

@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OauthFailed - RustDesk API</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.success-container {
text-align: center;
background: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
max-width: 400px;
width: 90%;
}
.checkmark {
color: #ba363a;
font-size: 4rem;
margin-bottom: 1rem;
}
h1 {
color: #333;
margin-bottom: 1rem;
}
p {
color: #666;
line-height: 1.6;
margin-bottom: 1.5rem;
}
.return-link {
display: inline-block;
padding: 10px 20px;
background-color: #ba363a;
color: white;
text-decoration: none;
border-radius: 5px;
transition: background-color 0.3s;
}
.return-link:hover {
background-color: #ba363a;
}
</style>
<link rel="stylesheet" href="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/font-awesome/6.0.0/css/all.min.css">
<script>
var lang = navigator.language || navigator.userLanguage || 'zh-CN';
var title = 'OauthFailed'
var msg = '{{.message}}'
var btn = 'Close'
document.writeln('<script src="/api/oauth/msg?lang=' + lang + '&msg=' + msg + '&title=OauthFailed"><\/script>');
</script>
</head>
<body>
<div class="success-container">
<i class="fas fa-triangle-exclamation checkmark"></i>
<h1 id="h1"></h1>
<p id="msg"></p>
<a href="javascript:window.close()" class="return-link" id="btn">Close</a>
</div>
<script>
document.title = title + ' - RustDesk API';
document.getElementById('h1').innerText = title;
document.getElementById('msg').innerText = msg;
</script>
</body>
</html>

View File

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OauthSuccess - RustDesk API</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.success-container {
text-align: center;
background: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
max-width: 400px;
width: 90%;
}
.checkmark {
color: #4CAF50;
font-size: 4rem;
margin-bottom: 1rem;
}
h1 {
color: #333;
margin-bottom: 1rem;
}
p {
color: #666;
line-height: 1.6;
margin-bottom: 1.5rem;
}
.return-link {
display: inline-block;
padding: 10px 20px;
background-color: #4CAF50;
color: white;
text-decoration: none;
border-radius: 5px;
transition: background-color 0.3s;
}
.return-link:hover {
background-color: #45a049;
}
</style>
<script>
var lang = navigator.language || navigator.userLanguage || 'zh-CN';
var title = 'OauthSuccess'
var msg = '{{.message}}'
var btn = 'Close'
document.writeln('<script src="/api/oauth/msg?lang=' + lang + '&msg=' + msg + '&title=OauthSuccess"><\/script>');
</script>
</head>
<body>
<div class="success-container">
<i class="fas fa-check-circle checkmark"></i>
<h1 id="h1"></h1>
<!-- <p>您已成功授权访问您的账户。</p>-->
<!-- <p>现在可以关闭本页面或返回应用继续操作。</p>-->
<a href="javascript:window.close()" class="return-link">Close</a>
</div>
<script>
document.title = title + ' - RustDesk API';
document.getElementById('h1').innerText = title;
document.getElementById('msg').innerText = msg;
</script>
</body>
</html>

View File

@@ -38,5 +38,21 @@
"asset": "assets/address_book.ttf"
}
]
},
{
"family": "DeviceGroup",
"fonts": [
{
"asset": "assets/device_group.ttf"
}
]
},
{
"family": "More",
"fonts": [
{
"asset": "assets/more.ttf"
}
]
}
]

Binary file not shown.

BIN
resources/web2/assets/assets/more.ttf vendored Normal file

Binary file not shown.

View File

@@ -32,7 +32,7 @@
<title>RustDesk</title>
<script src="/webclient-config/index.js"></script>
<link rel="manifest" href="manifest.json"/>
<script type="module" crossorigin src="js/dist/index.js?v=cabfd933"></script>
<script type="module" crossorigin src="js/dist/index.js?v=ddbe54f1"></script>
<link rel="modulepreload" href="js/dist/vendor.js?v=0b990c6e"/>
<style>
html,
@@ -42,6 +42,7 @@
margin: 0;
padding: 0;
}
#root {
background-repeat: no-repeat;
background-size: 100% auto;
@@ -63,6 +64,7 @@
justify-content: center;
padding: 26px;
}
.ant-spin {
position: absolute;
display: none;
@@ -78,8 +80,7 @@
text-align: center;
list-style: none;
opacity: 0;
-webkit-transition: -webkit-transform 0.3s
cubic-bezier(0.78, 0.14, 0.15, 0.86);
-webkit-transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
@@ -251,15 +252,16 @@
spanConsole.style.color = them === "dark" ? "#fff" : "#000";
}
const serviceWorkerVersion = "3267265270";
const serviceWorkerVersion = "461457302";
var scriptLoaded = false;
function loadMainDartJs() {
if (scriptLoaded) {
return;
}
scriptLoaded = true;
var scriptTag = document.createElement("script");
scriptTag.src = "main.dart.js?v=060a626e";
scriptTag.src = "main.dart.js?v=6d16cb80";
scriptTag.type = "application/javascript";
document.body.append(scriptTag);
}
@@ -281,6 +283,7 @@
}
});
}
if (!reg.active && (reg.installing || reg.waiting)) {
// No active web worker and we have installed or are installing
// one for the first time. Simply wait for it to activate.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,11 @@
window._gwen = {}
window._gwen.kv = {}
//fix 语言
if(!localStorage.getItem('wc-option:local:lang') && navigator.language){
localStorage.setItem('wc-option:local:lang', navigator.language.toLowerCase())
}
const storage_prefix = 'wc-'
const apiserver = localStorage.getItem('wc-api-server')
@@ -46,7 +52,7 @@ if (share_token) {
password: peer.tmppwd,
}*/
//修改location
window.location.href = `/webclient2/#/${peer.info.id}?password=${peer.tmppwd}`
window.location.href = `/webclient2/#/${peer.info.id}?password=${encodeURIComponent(peer.tmppwd)}`
}
})
}

163526
resources/web2/main.dart.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -3,7 +3,6 @@ package service
import (
"encoding/json"
"github.com/google/uuid"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/model"
"gorm.io/gorm"
"strings"
@@ -14,24 +13,24 @@ type AddressBookService struct {
func (s *AddressBookService) Info(id string) *model.AddressBook {
p := &model.AddressBook{}
global.DB.Where("id = ?", id).First(p)
DB.Where("id = ?", id).First(p)
return p
}
func (s *AddressBookService) InfoByUserIdAndId(userid uint, id string) *model.AddressBook {
p := &model.AddressBook{}
global.DB.Where("user_id = ? and id = ?", userid, id).First(p)
DB.Where("user_id = ? and id = ?", userid, id).First(p)
return p
}
func (s *AddressBookService) InfoByUserIdAndIdAndCid(userid uint, id string, cid uint) *model.AddressBook {
p := &model.AddressBook{}
global.DB.Where("user_id = ? and id = ? and collection_id = ?", userid, id, cid).First(p)
DB.Where("user_id = ? and id = ? and collection_id = ?", userid, id, cid).First(p)
return p
}
func (s *AddressBookService) InfoByRowId(id uint) *model.AddressBook {
p := &model.AddressBook{}
global.DB.Where("row_id = ?", id).First(p)
DB.Where("row_id = ?", id).First(p)
return p
}
func (s *AddressBookService) ListByUserId(userId, page, pageSize uint) (res *model.AddressBookList) {
@@ -49,14 +48,14 @@ func (s *AddressBookService) ListByUserIds(userIds []uint, page, pageSize uint)
// AddAddressBook
func (s *AddressBookService) AddAddressBook(ab *model.AddressBook) error {
return global.DB.Create(ab).Error
return DB.Create(ab).Error
}
// UpdateAddressBook
func (s *AddressBookService) UpdateAddressBook(abs []*model.AddressBook, userId uint) error {
//比较peers和数据库中的数据如果peers中的数据在数据库中不存在则添加如果存在则更新如果数据库中的数据在peers中不存在则删除
// 开始事务
tx := global.DB.Begin()
tx := DB.Begin()
//1. 获取数据库中的数据
var dbABs []*model.AddressBook
tx.Where("user_id = ?", userId).Find(&dbABs)
@@ -107,7 +106,7 @@ func (s *AddressBookService) List(page, pageSize uint, where func(tx *gorm.DB))
res = &model.AddressBookList{}
res.Page = int64(page)
res.PageSize = int64(pageSize)
tx := global.DB.Model(&model.AddressBook{})
tx := DB.Model(&model.AddressBook{})
if where != nil {
where(tx)
}
@@ -129,38 +128,38 @@ func (s *AddressBookService) FromPeer(peer *model.Peer) (a *model.AddressBook) {
// Create 创建
func (s *AddressBookService) Create(u *model.AddressBook) error {
res := global.DB.Create(u).Error
res := DB.Create(u).Error
return res
}
func (s *AddressBookService) Delete(u *model.AddressBook) error {
return global.DB.Delete(u).Error
return DB.Delete(u).Error
}
// Update 更新
func (s *AddressBookService) Update(u *model.AddressBook) error {
return global.DB.Model(u).Updates(u).Error
return DB.Model(u).Updates(u).Error
}
// UpdateByMap 更新
func (s *AddressBookService) UpdateByMap(u *model.AddressBook, data map[string]interface{}) error {
return global.DB.Model(u).Updates(data).Error
return DB.Model(u).Updates(data).Error
}
// UpdateAll 更新
func (s *AddressBookService) UpdateAll(u *model.AddressBook) error {
return global.DB.Model(u).Select("*").Omit("created_at").Updates(u).Error
return DB.Model(u).Select("*").Omit("created_at").Updates(u).Error
}
// ShareByWebClient 分享
func (s *AddressBookService) ShareByWebClient(m *model.ShareRecord) error {
m.ShareToken = uuid.New().String()
return global.DB.Create(m).Error
return DB.Create(m).Error
}
// SharedPeer
func (s *AddressBookService) SharedPeer(shareToken string) *model.ShareRecord {
m := &model.ShareRecord{}
global.DB.Where("share_token = ?", shareToken).First(m)
DB.Where("share_token = ?", shareToken).First(m)
return m
}
@@ -190,7 +189,7 @@ func (s *AddressBookService) ListCollection(page, pageSize uint, where func(tx *
res = &model.AddressBookCollectionList{}
res.Page = int64(page)
res.PageSize = int64(pageSize)
tx := global.DB.Model(&model.AddressBookCollection{})
tx := DB.Model(&model.AddressBookCollection{})
if where != nil {
where(tx)
}
@@ -200,7 +199,7 @@ func (s *AddressBookService) ListCollection(page, pageSize uint, where func(tx *
return
}
func (s *AddressBookService) ListCollectionByIds(ids []uint) (res []*model.AddressBookCollection) {
global.DB.Where("id in ?", ids).Find(&res)
DB.Where("id in ?", ids).Find(&res)
return res
}
@@ -212,20 +211,20 @@ func (s *AddressBookService) ListCollectionByUserId(userId uint) (res *model.Add
}
func (s *AddressBookService) CollectionInfoById(id uint) *model.AddressBookCollection {
p := &model.AddressBookCollection{}
global.DB.Where("id = ?", id).First(p)
DB.Where("id = ?", id).First(p)
return p
}
func (s *AddressBookService) CollectionReadRules(user *model.User) (res []*model.AddressBookCollectionRule) {
// personalRules
var personalRules []*model.AddressBookCollectionRule
tx2 := global.DB.Model(&model.AddressBookCollectionRule{})
tx2 := DB.Model(&model.AddressBookCollectionRule{})
tx2.Where("type = ? and to_id = ? and rule > 0", model.ShareAddressBookRuleTypePersonal, user.Id).Find(&personalRules)
res = append(res, personalRules...)
//group
var groupRules []*model.AddressBookCollectionRule
tx3 := global.DB.Model(&model.AddressBookCollectionRule{})
tx3 := DB.Model(&model.AddressBookCollectionRule{})
tx3.Where("type = ? and to_id = ? and rule > 0", model.ShareAddressBookRuleTypeGroup, user.GroupId).Find(&groupRules)
res = append(res, groupRules...)
return
@@ -238,7 +237,7 @@ func (s *AddressBookService) UserMaxRule(user *model.User, uid, cid uint) int {
}
max := 0
personalRules := &model.AddressBookCollectionRule{}
tx := global.DB.Model(personalRules)
tx := DB.Model(personalRules)
tx.Where("type = ? and collection_id = ? and to_id = ?", model.ShareAddressBookRuleTypePersonal, cid, user.Id).First(&personalRules)
if personalRules.Id != 0 {
max = personalRules.Rule
@@ -248,7 +247,7 @@ func (s *AddressBookService) UserMaxRule(user *model.User, uid, cid uint) int {
}
groupRules := &model.AddressBookCollectionRule{}
tx2 := global.DB.Model(groupRules)
tx2 := DB.Model(groupRules)
tx2.Where("type = ? and collection_id = ? and to_id = ?", model.ShareAddressBookRuleTypeGroup, cid, user.GroupId).First(&groupRules)
if groupRules.Id != 0 {
if groupRules.Rule > max {
@@ -272,16 +271,16 @@ func (s *AddressBookService) CheckUserFullControlPrivilege(user *model.User, uid
}
func (s *AddressBookService) CreateCollection(t *model.AddressBookCollection) error {
return global.DB.Create(t).Error
return DB.Create(t).Error
}
func (s *AddressBookService) UpdateCollection(t *model.AddressBookCollection) error {
return global.DB.Model(t).Updates(t).Error
return DB.Model(t).Updates(t).Error
}
func (s *AddressBookService) DeleteCollection(t *model.AddressBookCollection) error {
//删除集合下的所有规则、地址簿,再删除集合
tx := global.DB.Begin()
tx := DB.Begin()
tx.Where("collection_id = ?", t.Id).Delete(&model.AddressBookCollectionRule{})
tx.Where("collection_id = ?", t.Id).Delete(&model.AddressBook{})
tx.Delete(t)
@@ -290,23 +289,26 @@ func (s *AddressBookService) DeleteCollection(t *model.AddressBookCollection) er
func (s *AddressBookService) RuleInfoById(u uint) *model.AddressBookCollectionRule {
p := &model.AddressBookCollectionRule{}
global.DB.Where("id = ?", u).First(p)
DB.Where("id = ?", u).First(p)
return p
}
func (s *AddressBookService) RulePersonalInfoByToIdAndCid(toid, cid uint) *model.AddressBookCollectionRule {
return s.RuleInfoByToIdAndCid(model.ShareAddressBookRuleTypePersonal, toid, cid)
}
func (s *AddressBookService) RuleInfoByToIdAndCid(t int, toid, cid uint) *model.AddressBookCollectionRule {
p := &model.AddressBookCollectionRule{}
global.DB.Where("type = ? and to_id = ? and collection_id = ?", model.ShareAddressBookRuleTypePersonal, toid, cid).First(p)
DB.Where("type = ? and to_id = ? and collection_id = ?", t, toid, cid).First(p)
return p
}
func (s *AddressBookService) CreateRule(t *model.AddressBookCollectionRule) error {
return global.DB.Create(t).Error
return DB.Create(t).Error
}
func (s *AddressBookService) ListRules(page uint, size uint, f func(tx *gorm.DB)) *model.AddressBookCollectionRuleList {
res := &model.AddressBookCollectionRuleList{}
res.Page = int64(page)
res.PageSize = int64(size)
tx := global.DB.Model(&model.AddressBookCollectionRule{})
tx := DB.Model(&model.AddressBookCollectionRule{})
if f != nil {
f(tx)
}
@@ -317,11 +319,11 @@ func (s *AddressBookService) ListRules(page uint, size uint, f func(tx *gorm.DB)
}
func (s *AddressBookService) UpdateRule(t *model.AddressBookCollectionRule) error {
return global.DB.Model(t).Updates(t).Error
return DB.Model(t).Updates(t).Error
}
func (s *AddressBookService) DeleteRule(t *model.AddressBookCollectionRule) error {
return global.DB.Delete(t).Error
return DB.Delete(t).Error
}
// CheckCollectionOwner 检查Collection的所有者
@@ -336,5 +338,5 @@ func (s *AddressBookService) BatchUpdateTags(abs []*model.AddressBook, tags []st
ids = append(ids, ab.RowId)
}
tagsv, _ := json.Marshal(tags)
return global.DB.Model(&model.AddressBook{}).Where("row_id in ?", ids).Update("tags", tagsv).Error
return DB.Model(&model.AddressBook{}).Where("row_id in ?", ids).Update("tags", tagsv).Error
}

28
service/app.go Normal file
View File

@@ -0,0 +1,28 @@
package service
import (
"os"
"sync"
)
type AppService struct {
}
var version = ""
var once = &sync.Once{}
func (a *AppService) GetAppVersion() string {
if version != "" {
return version
}
once.Do(func() {
v, err := os.ReadFile("resources/version")
if err != nil {
return
}
version = string(v)
})
return version
}

33
service/app_test.go Normal file
View File

@@ -0,0 +1,33 @@
package service
import (
"sync"
"testing"
)
// TestGetAppVersion
func TestGetAppVersion(t *testing.T) {
s := &AppService{}
v := s.GetAppVersion()
// 打印结果
t.Logf("App Version: %s", v)
}
func TestMultipleGetAppVersion(t *testing.T) {
s := &AppService{}
//并发测试
// 使用 WaitGroup 等待所有 goroutine 完成
wg := sync.WaitGroup{}
wg.Add(10) // 启动 10 个 goroutine
// 启动 10 个 goroutine
for i := 0; i < 10; i++ {
go func() {
defer wg.Done() // 完成后减少计数
v := s.GetAppVersion()
// 打印结果
t.Logf("App Version: %s", v)
}()
}
// 等待所有 goroutine 完成
wg.Wait()
}

View File

@@ -1,7 +1,6 @@
package service
import (
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/model"
"gorm.io/gorm"
)
@@ -13,7 +12,7 @@ func (as *AuditService) AuditConnList(page, pageSize uint, where func(tx *gorm.D
res = &model.AuditConnList{}
res.Page = int64(page)
res.PageSize = int64(pageSize)
tx := global.DB.Model(&model.AuditConn{})
tx := DB.Model(&model.AuditConn{})
if where != nil {
where(tx)
}
@@ -25,36 +24,36 @@ func (as *AuditService) AuditConnList(page, pageSize uint, where func(tx *gorm.D
// Create 创建
func (as *AuditService) CreateAuditConn(u *model.AuditConn) error {
res := global.DB.Create(u).Error
res := DB.Create(u).Error
return res
}
func (as *AuditService) DeleteAuditConn(u *model.AuditConn) error {
return global.DB.Delete(u).Error
return DB.Delete(u).Error
}
// Update 更新
func (as *AuditService) UpdateAuditConn(u *model.AuditConn) error {
return global.DB.Model(u).Updates(u).Error
return DB.Model(u).Updates(u).Error
}
// InfoByPeerIdAndConnId
func (as *AuditService) InfoByPeerIdAndConnId(peerId string, connId int64) (res *model.AuditConn) {
res = &model.AuditConn{}
global.DB.Where("peer_id = ? and conn_id = ?", peerId, connId).First(res)
DB.Where("peer_id = ? and conn_id = ?", peerId, connId).First(res)
return
}
// ConnInfoById
func (as *AuditService) ConnInfoById(id uint) (res *model.AuditConn) {
res = &model.AuditConn{}
global.DB.Where("id = ?", id).First(res)
DB.Where("id = ?", id).First(res)
return
}
// FileInfoById
func (as *AuditService) FileInfoById(id uint) (res *model.AuditFile) {
res = &model.AuditFile{}
global.DB.Where("id = ?", id).First(res)
DB.Where("id = ?", id).First(res)
return
}
@@ -62,7 +61,7 @@ func (as *AuditService) AuditFileList(page, pageSize uint, where func(tx *gorm.D
res = &model.AuditFileList{}
res.Page = int64(page)
res.PageSize = int64(pageSize)
tx := global.DB.Model(&model.AuditFile{})
tx := DB.Model(&model.AuditFile{})
if where != nil {
where(tx)
}
@@ -74,22 +73,22 @@ func (as *AuditService) AuditFileList(page, pageSize uint, where func(tx *gorm.D
// CreateAuditFile
func (as *AuditService) CreateAuditFile(u *model.AuditFile) error {
res := global.DB.Create(u).Error
res := DB.Create(u).Error
return res
}
func (as *AuditService) DeleteAuditFile(u *model.AuditFile) error {
return global.DB.Delete(u).Error
return DB.Delete(u).Error
}
// Update 更新
func (as *AuditService) UpdateAuditFile(u *model.AuditFile) error {
return global.DB.Model(u).Updates(u).Error
return DB.Model(u).Updates(u).Error
}
func (as *AuditService) BatchDeleteAuditConn(ids []uint) error {
return global.DB.Where("id in (?)", ids).Delete(&model.AuditConn{}).Error
return DB.Where("id in (?)", ids).Delete(&model.AuditConn{}).Error
}
func (as *AuditService) BatchDeleteAuditFile(ids []uint) error {
return global.DB.Where("id in (?)", ids).Delete(&model.AuditFile{}).Error
return DB.Where("id in (?)", ids).Delete(&model.AuditFile{}).Error
}

View File

@@ -1,7 +1,6 @@
package service
import (
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/model"
"gorm.io/gorm"
)
@@ -12,7 +11,7 @@ type GroupService struct {
// InfoById 根据用户id取用户信息
func (us *GroupService) InfoById(id uint) *model.Group {
u := &model.Group{}
global.DB.Where("id = ?", id).First(u)
DB.Where("id = ?", id).First(u)
return u
}
@@ -20,7 +19,7 @@ func (us *GroupService) List(page, pageSize uint, where func(tx *gorm.DB)) (res
res = &model.GroupList{}
res.Page = int64(page)
res.PageSize = int64(pageSize)
tx := global.DB.Model(&model.Group{})
tx := DB.Model(&model.Group{})
if where != nil {
where(tx)
}
@@ -32,14 +31,47 @@ func (us *GroupService) List(page, pageSize uint, where func(tx *gorm.DB)) (res
// Create 创建
func (us *GroupService) Create(u *model.Group) error {
res := global.DB.Create(u).Error
res := DB.Create(u).Error
return res
}
func (us *GroupService) Delete(u *model.Group) error {
return global.DB.Delete(u).Error
return DB.Delete(u).Error
}
// Update 更新
func (us *GroupService) Update(u *model.Group) error {
return global.DB.Model(u).Updates(u).Error
return DB.Model(u).Updates(u).Error
}
// DeviceGroupInfoById 根据用户id取用户信息
func (us *GroupService) DeviceGroupInfoById(id uint) *model.DeviceGroup {
u := &model.DeviceGroup{}
DB.Where("id = ?", id).First(u)
return u
}
func (us *GroupService) DeviceGroupList(page, pageSize uint, where func(tx *gorm.DB)) (res *model.DeviceGroupList) {
res = &model.DeviceGroupList{}
res.Page = int64(page)
res.PageSize = int64(pageSize)
tx := DB.Model(&model.DeviceGroup{})
if where != nil {
where(tx)
}
tx.Count(&res.Total)
tx.Scopes(Paginate(page, pageSize))
tx.Find(&res.DeviceGroups)
return
}
func (us *GroupService) DeviceGroupCreate(u *model.DeviceGroup) error {
res := DB.Create(u).Error
return res
}
func (us *GroupService) DeviceGroupDelete(u *model.DeviceGroup) error {
return DB.Delete(u).Error
}
func (us *GroupService) DeviceGroupUpdate(u *model.DeviceGroup) error {
return DB.Model(u).Updates(u).Error
}

View File

@@ -2,19 +2,23 @@ package service
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net/url"
"os"
"strconv"
"strings"
"github.com/go-ldap/ldap/v3"
"github.com/lejianwen/rustdesk-api/v2/config"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/model"
)
var (
ErrUrlParseFailed = errors.New("UrlParseFailed")
ErrFileReadFailed = errors.New("FileReadFailed")
ErrLdapNotEnabled = errors.New("LdapNotEnabled")
ErrLdapUserDisabled = errors.New("UserDisabledAtLdap")
ErrLdapUserNotFound = errors.New("UserNotFound")
@@ -26,6 +30,7 @@ var (
ErrLdapBindFailed = errors.New("LdapBindFailed")
ErrLdapToLocalUserFailed = errors.New("LdapToLocalUserFailed")
ErrLdapCreateUserFailed = errors.New("LdapCreateUserFailed")
ErrLdapPasswordNotMatch = errors.New("PasswordNotMatch")
)
// LdapService is responsible for LDAP authentication and user synchronization.
@@ -68,21 +73,38 @@ func (lu *LdapUser) ToUser(u *model.User) *model.User {
// connectAndBind creates an LDAP connection, optionally starts TLS, and then binds using the provided credentials.
func (ls *LdapService) connectAndBind(cfg *config.Ldap, username, password string) (*ldap.Conn, error) {
conn, err := ldap.DialURL(cfg.Url)
u, err := url.Parse(cfg.Url)
if err != nil {
return nil, errors.Join(ErrUrlParseFailed, err)
}
var conn *ldap.Conn
if u.Scheme == "ldaps" {
// WARNING: InsecureSkipVerify: true is not recommended for production
tlsConfig := &tls.Config{InsecureSkipVerify: !cfg.TlsVerify}
if cfg.TlsCaFile != "" {
caCert, err := os.ReadFile(cfg.TlsCaFile)
if err != nil {
return nil, errors.Join(ErrFileReadFailed, err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
return nil, errors.Join(ErrLdapTlsFailed, errors.New("failed to append CA certificate"))
}
tlsConfig.RootCAs = caCertPool
}
conn, err = ldap.DialURL(cfg.Url, ldap.DialWithTLSConfig(tlsConfig))
} else {
conn, err = ldap.DialURL(cfg.Url)
}
if err != nil {
return nil, errors.Join(ErrLdapConnectFailed, err)
}
if cfg.TLS {
// WARNING: InsecureSkipVerify: true is not recommended for production
if err = conn.StartTLS(&tls.Config{InsecureSkipVerify: !cfg.TlsVerify}); err != nil {
conn.Close()
return nil, errors.Join(ErrLdapTlsFailed, err)
}
}
// Bind as the "service" user
if err = conn.Bind(username, password); err != nil {
fmt.Println("Bind failed")
conn.Close()
return nil, errors.Join(ErrLdapBindService, err)
}
@@ -98,7 +120,7 @@ func (ls *LdapService) connectAndBindAdmin(cfg *config.Ldap) (*ldap.Conn, error)
func (ls *LdapService) verifyCredentials(cfg *config.Ldap, username, password string) error {
ldapConn, err := ls.connectAndBind(cfg, username, password)
if err != nil {
return err
return ErrLdapPasswordNotMatch
}
defer ldapConn.Close()
return nil
@@ -114,7 +136,11 @@ func (ls *LdapService) Authenticate(username, password string) (*model.User, err
if !ldapUser.Enabled {
return nil, ErrLdapUserDisabled
}
cfg := &global.Config.Ldap
cfg := &Config.Ldap
err = ls.verifyCredentials(cfg, ldapUser.Dn, password)
if err != nil {
return nil, err
}
user, err := ls.mapToLocalUser(cfg, ldapUser)
if err != nil {
return nil, errors.Join(ErrLdapToLocalUserFailed, err)
@@ -135,7 +161,7 @@ func (ls *LdapService) mapToLocalUser(cfg *config.Ldap, lu *LdapUser) (*model.Us
// If needed, you can set a random password here.
newUser.IsAdmin = &isAdmin
newUser.GroupId = 1
if err := global.DB.Create(newUser).Error; err != nil {
if err := DB.Create(newUser).Error; err != nil {
return nil, errors.Join(ErrLdapCreateUserFailed, err)
}
return userService.InfoByUsername(lu.Username), nil
@@ -164,7 +190,7 @@ func (ls *LdapService) mapToLocalUser(cfg *config.Ldap, lu *LdapUser) (*model.Us
// IsUsernameExists checks if a username exists in LDAP (can be useful for local registration checks).
func (ls *LdapService) IsUsernameExists(username string) bool {
cfg := &global.Config.Ldap
cfg := &Config.Ldap
if !cfg.Enable {
return false
}
@@ -177,7 +203,7 @@ func (ls *LdapService) IsUsernameExists(username string) bool {
// IsEmailExists checks if an email exists in LDAP (can be useful for local registration checks).
func (ls *LdapService) IsEmailExists(email string) bool {
cfg := &global.Config.Ldap
cfg := &Config.Ldap
if !cfg.Enable {
return false
}
@@ -190,7 +216,7 @@ func (ls *LdapService) IsEmailExists(email string) bool {
// GetUserInfoByUsernameLdap returns the user info from LDAP for the given username.
func (ls *LdapService) GetUserInfoByUsernameLdap(username string) (*LdapUser, error) {
cfg := &global.Config.Ldap
cfg := &Config.Ldap
if !cfg.Enable {
return nil, ErrLdapNotEnabled
}
@@ -210,12 +236,12 @@ func (ls *LdapService) GetUserInfoByUsernameLocal(username string) (*model.User,
if err != nil {
return &model.User{}, err
}
return ls.mapToLocalUser(&global.Config.Ldap, ldapUser)
return ls.mapToLocalUser(&Config.Ldap, ldapUser)
}
// GetUserInfoByEmailLdap returns the user info from LDAP for the given email.
func (ls *LdapService) GetUserInfoByEmailLdap(email string) (*LdapUser, error) {
cfg := &global.Config.Ldap
cfg := &Config.Ldap
if !cfg.Enable {
return nil, ErrLdapNotEnabled
}
@@ -235,7 +261,7 @@ func (ls *LdapService) GetUserInfoByEmailLocal(email string) (*model.User, error
if err != nil {
return &model.User{}, err
}
return ls.mapToLocalUser(&global.Config.Ldap, ldapUser)
return ls.mapToLocalUser(&Config.Ldap, ldapUser)
}
// usernameSearchResult returns the search result for the given username.
@@ -453,12 +479,12 @@ func (ls *LdapService) isUserEnabled(cfg *config.Ldap, ldapUser *LdapUser) bool
// Account is disabled if the ACCOUNTDISABLE flag (0x2) is set
const ACCOUNTDISABLE = 0x2
ldapUser.Enabled = (userAccountControl&ACCOUNTDISABLE == 0)
ldapUser.Enabled = userAccountControl&ACCOUNTDISABLE == 0
return ldapUser.Enabled
}
// For other attributes, perform a direct comparison with the expected value
ldapUser.Enabled = (ldapUser.EnableAttrValue == enableAttrValue)
ldapUser.Enabled = ldapUser.EnableAttrValue == enableAttrValue
return ldapUser.Enabled
}

View File

@@ -1,7 +1,6 @@
package service
import (
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/model"
"gorm.io/gorm"
)
@@ -12,7 +11,7 @@ type LoginLogService struct {
// InfoById 根据用户id取用户信息
func (us *LoginLogService) InfoById(id uint) *model.LoginLog {
u := &model.LoginLog{}
global.DB.Where("id = ?", id).First(u)
DB.Where("id = ?", id).First(u)
return u
}
@@ -20,7 +19,7 @@ func (us *LoginLogService) List(page, pageSize uint, where func(tx *gorm.DB)) (r
res = &model.LoginLogList{}
res.Page = int64(page)
res.PageSize = int64(pageSize)
tx := global.DB.Model(&model.LoginLog{})
tx := DB.Model(&model.LoginLog{})
if where != nil {
where(tx)
}
@@ -32,20 +31,20 @@ func (us *LoginLogService) List(page, pageSize uint, where func(tx *gorm.DB)) (r
// Create 创建
func (us *LoginLogService) Create(u *model.LoginLog) error {
res := global.DB.Create(u).Error
res := DB.Create(u).Error
return res
}
func (us *LoginLogService) Delete(u *model.LoginLog) error {
return global.DB.Delete(u).Error
return DB.Delete(u).Error
}
// Update 更新
func (us *LoginLogService) Update(u *model.LoginLog) error {
return global.DB.Model(u).Updates(u).Error
return DB.Model(u).Updates(u).Error
}
func (us *LoginLogService) BatchDelete(ids []uint) error {
return global.DB.Where("id in (?)", ids).Delete(&model.LoginLog{}).Error
return DB.Where("id in (?)", ids).Delete(&model.LoginLog{}).Error
}
func (us *LoginLogService) SoftDelete(l *model.LoginLog) error {
@@ -54,5 +53,5 @@ func (us *LoginLogService) SoftDelete(l *model.LoginLog) error {
}
func (us *LoginLogService) BatchSoftDelete(uid uint, ids []uint) error {
return global.DB.Model(&model.LoginLog{}).Where("user_id = ? and id in (?)", uid, ids).Update("is_deleted", model.IsDeletedYes).Error
return DB.Model(&model.LoginLog{}).Where("user_id = ? and id in (?)", uid, ids).Update("is_deleted", model.IsDeletedYes).Error
}

View File

@@ -4,7 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/utils"
"golang.org/x/oauth2"
@@ -45,6 +45,8 @@ type OauthCacheItem struct {
Username string `json:"username"`
Name string `json:"name"`
Email string `json:"email"`
Verifier string `json:"verifier"` // used for oauth pkce
Nonce string `json:"nonce"`
}
func (oci *OauthCacheItem) ToOauthUser() *model.OauthUser {
@@ -81,10 +83,9 @@ func (os *OauthService) GetOauthCache(key string) *OauthCacheItem {
func (os *OauthService) SetOauthCache(key string, item *OauthCacheItem, expire uint) {
OauthCache.Store(key, item)
if expire > 0 {
go func() {
time.Sleep(time.Duration(expire) * time.Second)
time.AfterFunc(time.Duration(expire)*time.Second, func() {
os.DeleteOauthCache(key)
}()
})
}
}
@@ -92,151 +93,192 @@ func (os *OauthService) DeleteOauthCache(key string) {
OauthCache.Delete(key)
}
func (os *OauthService) BeginAuth(op string) (error error, code, url string) {
code = utils.RandomString(10) + strconv.FormatInt(time.Now().Unix(), 10)
if op == string(model.OauthTypeWebauth) {
url = global.Config.Rustdesk.ApiServer + "/_admin/#/oauth/" + code
func (os *OauthService) BeginAuth(op string) (error error, state, verifier, nonce, url string) {
state = utils.RandomString(10) + strconv.FormatInt(time.Now().Unix(), 10)
verifier = ""
nonce = ""
if op == model.OauthTypeWebauth {
url = Config.Rustdesk.ApiServer + "/_admin/#/oauth/" + state
//url = "http://localhost:8888/_admin/#/oauth/" + code
return nil, code, url
return nil, state, verifier, nonce, url
}
err, _, oauthConfig := os.GetOauthConfig(op)
err, oauthInfo, oauthConfig, _ := os.GetOauthConfig(op)
if err == nil {
return err, code, oauthConfig.AuthCodeURL(code)
extras := make([]oauth2.AuthCodeOption, 0, 3)
nonce = utils.RandomString(10)
extras = append(extras, oauth2.SetAuthURLParam("nonce", nonce))
if oauthInfo.PkceEnable != nil && *oauthInfo.PkceEnable {
extras = append(extras, oauth2.AccessTypeOffline)
verifier = oauth2.GenerateVerifier()
switch oauthInfo.PkceMethod {
case model.PKCEMethodS256:
extras = append(extras, oauth2.S256ChallengeOption(verifier))
case model.PKCEMethodPlain:
// oauth2 does not have a plain challenge option, so we add it manually
extras = append(extras, oauth2.SetAuthURLParam("code_challenge_method", "plain"), oauth2.SetAuthURLParam("code_challenge", verifier))
}
}
return err, code, ""
return err, state, verifier, nonce, oauthConfig.AuthCodeURL(state, extras...)
}
// Method to fetch OIDC configuration dynamically
func (os *OauthService) FetchOidcEndpoint(issuer string) (error, OidcEndpoint) {
configURL := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration"
return err, state, verifier, nonce, ""
}
func (os *OauthService) FetchOidcProvider(issuer string) (error, *oidc.Provider) {
// Get the HTTP client (with or without proxy based on configuration)
client := getHTTPClientWithProxy()
resp, err := client.Get(configURL)
ctx := oidc.ClientContext(context.Background(), client)
provider, err := oidc.NewProvider(ctx, issuer)
if err != nil {
return errors.New("failed to fetch OIDC configuration"), OidcEndpoint{}
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New("OIDC configuration not found, status code: %d"), OidcEndpoint{}
return err, nil
}
var endpoint OidcEndpoint
if err := json.NewDecoder(resp.Body).Decode(&endpoint); err != nil {
return errors.New("failed to parse OIDC configuration"), OidcEndpoint{}
return nil, provider
}
return nil, endpoint
}
func (os *OauthService) FetchOidcEndpointByOp(op string) (error, OidcEndpoint) {
oauthInfo := os.InfoByOp(op)
if oauthInfo.Issuer == "" {
return errors.New("issuer is empty"), OidcEndpoint{}
}
return os.FetchOidcEndpoint(oauthInfo.Issuer)
func (os *OauthService) GithubProvider() *oidc.Provider {
return (&oidc.ProviderConfig{
IssuerURL: "",
AuthURL: github.Endpoint.AuthURL,
TokenURL: github.Endpoint.TokenURL,
DeviceAuthURL: github.Endpoint.DeviceAuthURL,
UserInfoURL: model.UserEndpointGithub,
JWKSURL: "",
Algorithms: nil,
}).NewProvider(context.Background())
}
// GetOauthConfig retrieves the OAuth2 configuration based on the provider name
func (os *OauthService) GetOauthConfig(op string) (err error, oauthInfo *model.Oauth, oauthConfig *oauth2.Config) {
err, oauthInfo, oauthConfig = os.getOauthConfigGeneral(op)
if err != nil {
return err, nil, nil
func (os *OauthService) GetOauthConfig(op string) (err error, oauthInfo *model.Oauth, oauthConfig *oauth2.Config, provider *oidc.Provider) {
//err, oauthInfo, oauthConfig = os.getOauthConfigGeneral(op)
oauthInfo = os.InfoByOp(op)
if oauthInfo.Id == 0 || oauthInfo.ClientId == "" || oauthInfo.ClientSecret == "" {
return errors.New("ConfigNotFound"), nil, nil, nil
}
// If the redirect URL is empty, use the default redirect URL
if oauthInfo.RedirectUrl == "" {
oauthInfo.RedirectUrl = Config.Rustdesk.ApiServer + "/api/oidc/callback"
}
oauthConfig = &oauth2.Config{
ClientID: oauthInfo.ClientId,
ClientSecret: oauthInfo.ClientSecret,
RedirectURL: oauthInfo.RedirectUrl,
}
// Maybe should validate the oauthConfig here
oauthType := oauthInfo.OauthType
err = model.ValidateOauthType(oauthType)
if err != nil {
return err, nil, nil
return err, nil, nil, nil
}
switch oauthType {
case model.OauthTypeGithub:
oauthConfig.Endpoint = github.Endpoint
oauthConfig.Scopes = []string{"read:user", "user:email"}
provider = os.GithubProvider()
//case model.OauthTypeGoogle: //google单独出来可以少一次FetchOidcEndpoint请求
// oauthConfig.Endpoint = google.Endpoint
// oauthConfig.Scopes = os.constructScopes(oauthInfo.Scopes)
case model.OauthTypeOidc, model.OauthTypeGoogle:
var endpoint OidcEndpoint
err, endpoint = os.FetchOidcEndpoint(oauthInfo.Issuer)
err, provider = os.FetchOidcProvider(oauthInfo.Issuer)
if err != nil {
return err, nil, nil
return err, nil, nil, nil
}
oauthConfig.Endpoint = oauth2.Endpoint{AuthURL: endpoint.AuthURL, TokenURL: endpoint.TokenURL}
oauthConfig.Endpoint = provider.Endpoint()
oauthConfig.Scopes = os.constructScopes(oauthInfo.Scopes)
default:
return errors.New("unsupported OAuth type"), nil, nil
}
return nil, oauthInfo, oauthConfig
}
// GetOauthConfig retrieves the OAuth2 configuration based on the provider name
func (os *OauthService) getOauthConfigGeneral(op string) (err error, oauthInfo *model.Oauth, oauthConfig *oauth2.Config) {
oauthInfo = os.InfoByOp(op)
if oauthInfo.Id == 0 || oauthInfo.ClientId == "" || oauthInfo.ClientSecret == "" {
return errors.New("ConfigNotFound"), nil, nil
}
// If the redirect URL is empty, use the default redirect URL
if oauthInfo.RedirectUrl == "" {
oauthInfo.RedirectUrl = global.Config.Rustdesk.ApiServer + "/api/oidc/callback"
}
return nil, oauthInfo, &oauth2.Config{
ClientID: oauthInfo.ClientId,
ClientSecret: oauthInfo.ClientSecret,
RedirectURL: oauthInfo.RedirectUrl,
return errors.New("unsupported OAuth type"), nil, nil, nil
}
return nil, oauthInfo, oauthConfig, provider
}
func getHTTPClientWithProxy() *http.Client {
//todo add timeout
if global.Config.Proxy.Enable {
if global.Config.Proxy.Host == "" {
global.Logger.Warn("Proxy is enabled but proxy host is empty.")
//add timeout 30s
timeout := time.Duration(60) * time.Second
if Config.Proxy.Enable {
if Config.Proxy.Host == "" {
Logger.Warn("Proxy is enabled but proxy host is empty.")
return http.DefaultClient
}
proxyURL, err := url.Parse(global.Config.Proxy.Host)
proxyURL, err := url.Parse(Config.Proxy.Host)
if err != nil {
global.Logger.Warn("Invalid proxy URL: ", err)
Logger.Warn("Invalid proxy URL: ", err)
return http.DefaultClient
}
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}
return &http.Client{Transport: transport}
return &http.Client{Transport: transport, Timeout: timeout}
}
return http.DefaultClient
}
func (os *OauthService) callbackBase(oauthConfig *oauth2.Config, code string, userEndpoint string, userData interface{}) (err error, client *http.Client) {
func (os *OauthService) callbackBase(oauthConfig *oauth2.Config, provider *oidc.Provider, code string, verifier string, nonce string, userData interface{}) (err error, client *http.Client) {
// 设置代理客户端
httpClient := getHTTPClientWithProxy()
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient)
// 使用 code 换取 token
var token *oauth2.Token
token, err = oauthConfig.Exchange(ctx, code)
exchangeOpts := make([]oauth2.AuthCodeOption, 0, 1)
if verifier != "" {
exchangeOpts = append(exchangeOpts, oauth2.VerifierOption(verifier))
}
token, err := oauthConfig.Exchange(ctx, code, exchangeOpts...)
if err != nil {
global.Logger.Warn("oauthConfig.Exchange() failed: ", err)
Logger.Warn("oauthConfig.Exchange() failed: ", err)
return errors.New("GetOauthTokenError"), nil
}
// 获取 ID Token github没有id_token
rawIDToken, ok := token.Extra("id_token").(string)
if ok && rawIDToken != "" {
// 验证 ID Token
v := provider.Verifier(&oidc.Config{ClientID: oauthConfig.ClientID})
idToken, err2 := v.Verify(ctx, rawIDToken)
if err2 != nil {
Logger.Warn("IdTokenVerifyError: ", err2)
return errors.New("IdTokenVerifyError"), nil
}
if nonce != "" {
// 验证 nonce
var claims struct {
Nonce string `json:"nonce"`
}
if err2 = idToken.Claims(&claims); err2 != nil {
Logger.Warn("Failed to parse ID Token claims: ", err)
return errors.New("IDTokenClaimsError"), nil
}
if claims.Nonce != nonce {
Logger.Warn("Nonce does not match")
return errors.New("NonceDoesNotMatch"), nil
}
}
}
// 获取用户信息
client = oauthConfig.Client(ctx, token)
resp, err := client.Get(userEndpoint)
resp, err := client.Get(provider.UserInfoEndpoint())
if err != nil {
global.Logger.Warn("failed getting user info: ", err)
Logger.Warn("failed getting user info: ", err)
return errors.New("GetOauthUserInfoError"), nil
}
defer func() {
if closeErr := resp.Body.Close(); closeErr != nil {
global.Logger.Warn("failed closing response body: ", closeErr)
Logger.Warn("failed closing response body: ", closeErr)
}
}()
// 解析用户信息
if err = json.NewDecoder(resp.Body).Decode(userData); err != nil {
global.Logger.Warn("failed decoding user info: ", err)
Logger.Warn("failed decoding user info: ", err)
return errors.New("DecodeOauthUserInfoError"), nil
}
@@ -244,9 +286,9 @@ func (os *OauthService) callbackBase(oauthConfig *oauth2.Config, code string, us
}
// githubCallback github回调
func (os *OauthService) githubCallback(oauthConfig *oauth2.Config, code string) (error, *model.OauthUser) {
func (os *OauthService) githubCallback(oauthConfig *oauth2.Config, provider *oidc.Provider, code, verifier, nonce string) (error, *model.OauthUser) {
var user = &model.GithubUser{}
err, client := os.callbackBase(oauthConfig, code, model.UserEndpointGithub, user)
err, client := os.callbackBase(oauthConfig, provider, code, verifier, nonce, user)
if err != nil {
return err, nil
}
@@ -258,19 +300,17 @@ func (os *OauthService) githubCallback(oauthConfig *oauth2.Config, code string)
}
// oidcCallback oidc回调, 通过code获取用户信息
func (os *OauthService) oidcCallback(oauthConfig *oauth2.Config, code string, userInfoEndpoint string) (error, *model.OauthUser) {
func (os *OauthService) oidcCallback(oauthConfig *oauth2.Config, provider *oidc.Provider, code, verifier, nonce string) (error, *model.OauthUser) {
var user = &model.OidcUser{}
if err, _ := os.callbackBase(oauthConfig, code, userInfoEndpoint, user); err != nil {
if err, _ := os.callbackBase(oauthConfig, provider, code, verifier, nonce, user); err != nil {
return err, nil
}
return nil, user.ToOauthUser()
}
// Callback: Get user information by code and op(Oauth provider)
func (os *OauthService) Callback(code string, op string) (err error, oauthUser *model.OauthUser) {
var oauthInfo *model.Oauth
var oauthConfig *oauth2.Config
err, oauthInfo, oauthConfig = os.GetOauthConfig(op)
func (os *OauthService) Callback(code, verifier, op, nonce string) (err error, oauthUser *model.OauthUser) {
err, oauthInfo, oauthConfig, provider := os.GetOauthConfig(op)
// oauthType is already validated in GetOauthConfig
if err != nil {
return err, nil
@@ -278,13 +318,9 @@ func (os *OauthService) Callback(code string, op string) (err error, oauthUser *
oauthType := oauthInfo.OauthType
switch oauthType {
case model.OauthTypeGithub:
err, oauthUser = os.githubCallback(oauthConfig, code)
err, oauthUser = os.githubCallback(oauthConfig, provider, code, verifier, nonce)
case model.OauthTypeOidc, model.OauthTypeGoogle:
err, endpoint := os.FetchOidcEndpoint(oauthInfo.Issuer)
if err != nil {
return err, nil
}
err, oauthUser = os.oidcCallback(oauthConfig, code, endpoint.UserInfo)
err, oauthUser = os.oidcCallback(oauthConfig, provider, code, verifier, nonce)
default:
return errors.New("unsupported OAuth type"), nil
}
@@ -293,7 +329,7 @@ func (os *OauthService) Callback(code string, op string) (err error, oauthUser *
func (os *OauthService) UserThirdInfo(op string, openId string) *model.UserThird {
ut := &model.UserThird{}
global.DB.Where("open_id = ? and op = ?", openId, op).First(ut)
DB.Where("open_id = ? and op = ?", openId, op).First(ut)
return ut
}
@@ -305,7 +341,7 @@ func (os *OauthService) BindOauthUser(userId uint, oauthUser *model.OauthUser, o
return err
}
utr.FromOauthUser(userId, oauthUser, oauthType, op)
return global.DB.Create(utr).Error
return DB.Create(utr).Error
}
// UnBindOauthUser: Unbind third party account
@@ -315,25 +351,25 @@ func (os *OauthService) UnBindOauthUser(userId uint, op string) error {
// UnBindThird: Unbind third party account
func (os *OauthService) UnBindThird(op string, userId uint) error {
return global.DB.Where("user_id = ? and op = ?", userId, op).Delete(&model.UserThird{}).Error
return DB.Where("user_id = ? and op = ?", userId, op).Delete(&model.UserThird{}).Error
}
// DeleteUserByUserId: When user is deleted, delete all third party bindings
func (os *OauthService) DeleteUserByUserId(userId uint) error {
return global.DB.Where("user_id = ?", userId).Delete(&model.UserThird{}).Error
return DB.Where("user_id = ?", userId).Delete(&model.UserThird{}).Error
}
// InfoById 根据id获取Oauth信息
func (os *OauthService) InfoById(id uint) *model.Oauth {
oauthInfo := &model.Oauth{}
global.DB.Where("id = ?", id).First(oauthInfo)
DB.Where("id = ?", id).First(oauthInfo)
return oauthInfo
}
// InfoByOp 根据op获取Oauth信息
func (os *OauthService) InfoByOp(op string) *model.Oauth {
oauthInfo := &model.Oauth{}
global.DB.Where("op = ?", op).First(oauthInfo)
DB.Where("op = ?", op).First(oauthInfo)
return oauthInfo
}
@@ -356,7 +392,7 @@ func (os *OauthService) List(page, pageSize uint, where func(tx *gorm.DB)) (res
res = &model.OauthList{}
res.Page = int64(page)
res.PageSize = int64(pageSize)
tx := global.DB.Model(&model.Oauth{})
tx := DB.Model(&model.Oauth{})
if where != nil {
where(tx)
}
@@ -369,7 +405,7 @@ func (os *OauthService) List(page, pageSize uint, where func(tx *gorm.DB)) (res
// GetTypeByOp 根据op获取OauthType
func (os *OauthService) GetTypeByOp(op string) (error, string) {
oauthInfo := &model.Oauth{}
if global.DB.Where("op = ?", op).First(oauthInfo).Error != nil {
if DB.Where("op = ?", op).First(oauthInfo).Error != nil {
return fmt.Errorf("OAuth provider with op '%s' not found", op), ""
}
return nil, oauthInfo.OauthType
@@ -387,7 +423,7 @@ func (os *OauthService) ValidateOauthProvider(op string) error {
func (os *OauthService) IsOauthProviderExist(op string) bool {
oauthInfo := &model.Oauth{}
// 使用 Gorm 的 Take 方法查找符合条件的记录
if err := global.DB.Where("op = ?", op).Take(oauthInfo).Error; err != nil {
if err := DB.Where("op = ?", op).Take(oauthInfo).Error; err != nil {
return false
}
return true
@@ -399,11 +435,11 @@ func (os *OauthService) Create(oauthInfo *model.Oauth) error {
if err != nil {
return err
}
res := global.DB.Create(oauthInfo).Error
res := DB.Create(oauthInfo).Error
return res
}
func (os *OauthService) Delete(oauthInfo *model.Oauth) error {
return global.DB.Delete(oauthInfo).Error
return DB.Delete(oauthInfo).Error
}
// Update 更新
@@ -412,13 +448,13 @@ func (os *OauthService) Update(oauthInfo *model.Oauth) error {
if err != nil {
return err
}
return global.DB.Model(oauthInfo).Updates(oauthInfo).Error
return DB.Model(oauthInfo).Updates(oauthInfo).Error
}
// GetOauthProviders 获取所有的provider
func (os *OauthService) GetOauthProviders() []string {
var res []string
global.DB.Model(&model.Oauth{}).Pluck("op", &res)
DB.Model(&model.Oauth{}).Pluck("op", &res)
return res
}

View File

@@ -1,7 +1,6 @@
package service
import (
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/model"
"gorm.io/gorm"
)
@@ -12,24 +11,24 @@ type PeerService struct {
// FindById 根据id查找
func (ps *PeerService) FindById(id string) *model.Peer {
p := &model.Peer{}
global.DB.Where("id = ?", id).First(p)
DB.Where("id = ?", id).First(p)
return p
}
func (ps *PeerService) FindByUuid(uuid string) *model.Peer {
p := &model.Peer{}
global.DB.Where("uuid = ?", uuid).First(p)
DB.Where("uuid = ?", uuid).First(p)
return p
}
func (ps *PeerService) InfoByRowId(id uint) *model.Peer {
p := &model.Peer{}
global.DB.Where("row_id = ?", id).First(p)
DB.Where("row_id = ?", id).First(p)
return p
}
// FindByUserIdAndUuid 根据用户id和uuid查找peer
func (ps *PeerService) FindByUserIdAndUuid(uuid string, userId uint) *model.Peer {
p := &model.Peer{}
global.DB.Where("uuid = ? and user_id = ?", uuid, userId).First(p)
DB.Where("uuid = ? and user_id = ?", uuid, userId).First(p)
return p
}
@@ -43,7 +42,7 @@ func (ps *PeerService) UuidBindUserId(deviceId string, uuid string, userId uint)
} else {
// 不存在则创建
/*if deviceId != "" {
global.DB.Create(&model.Peer{
DB.Create(&model.Peer{
Id: deviceId,
Uuid: uuid,
UserId: userId,
@@ -56,13 +55,13 @@ func (ps *PeerService) UuidBindUserId(deviceId string, uuid string, userId uint)
func (ps *PeerService) UuidUnbindUserId(uuid string, userId uint) {
peer := ps.FindByUserIdAndUuid(uuid, userId)
if peer.RowId > 0 {
global.DB.Model(peer).Update("user_id", 0)
DB.Model(peer).Update("user_id", 0)
}
}
// EraseUserId 清除用户id, 用于用户删除
func (ps *PeerService) EraseUserId(userId uint) error {
return global.DB.Model(&model.Peer{}).Where("user_id = ?", userId).Update("user_id", 0).Error
return DB.Model(&model.Peer{}).Where("user_id = ?", userId).Update("user_id", 0).Error
}
// ListByUserIds 根据用户id取列表
@@ -70,7 +69,7 @@ func (ps *PeerService) ListByUserIds(userIds []uint, page, pageSize uint) (res *
res = &model.PeerList{}
res.Page = int64(page)
res.PageSize = int64(pageSize)
tx := global.DB.Model(&model.Peer{})
tx := DB.Model(&model.Peer{})
tx.Where("user_id in (?)", userIds)
tx.Count(&res.Total)
tx.Scopes(Paginate(page, pageSize))
@@ -82,7 +81,7 @@ func (ps *PeerService) List(page, pageSize uint, where func(tx *gorm.DB)) (res *
res = &model.PeerList{}
res.Page = int64(page)
res.PageSize = int64(pageSize)
tx := global.DB.Model(&model.Peer{})
tx := DB.Model(&model.Peer{})
if where != nil {
where(tx)
}
@@ -106,14 +105,14 @@ func (ps *PeerService) ListFilterByUserId(page, pageSize uint, where func(tx *go
// Create 创建
func (ps *PeerService) Create(u *model.Peer) error {
res := global.DB.Create(u).Error
res := DB.Create(u).Error
return res
}
// Delete 删除, 同时也应该删除token
func (ps *PeerService) Delete(u *model.Peer) error {
uuid := u.Uuid
err := global.DB.Delete(u).Error
err := DB.Delete(u).Error
if err != nil {
return err
}
@@ -124,16 +123,23 @@ func (ps *PeerService) Delete(u *model.Peer) error {
// GetUuidListByIDs 根据ids获取uuid列表
func (ps *PeerService) GetUuidListByIDs(ids []uint) ([]string, error) {
var uuids []string
err := global.DB.Model(&model.Peer{}).
err := DB.Model(&model.Peer{}).
Where("row_id in (?)", ids).
Pluck("uuid", &uuids).Error
return uuids, err
//过滤uuids中的空字符串
var newUuids []string
for _, uuid := range uuids {
if uuid != "" {
newUuids = append(newUuids, uuid)
}
}
return newUuids, err
}
// BatchDelete 批量删除, 同时也应该删除token
func (ps *PeerService) BatchDelete(ids []uint) error {
uuids, err := ps.GetUuidListByIDs(ids)
err = global.DB.Where("row_id in (?)", ids).Delete(&model.Peer{}).Error
err = DB.Where("row_id in (?)", ids).Delete(&model.Peer{}).Error
if err != nil {
return err
}
@@ -143,5 +149,5 @@ func (ps *PeerService) BatchDelete(ids []uint) error {
// Update 更新
func (ps *PeerService) Update(u *model.Peer) error {
return global.DB.Model(u).Updates(u).Error
return DB.Model(u).Updates(u).Error
}

View File

@@ -2,7 +2,6 @@ package service
import (
"fmt"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/model"
"net"
"time"
@@ -15,7 +14,7 @@ func (is *ServerCmdService) List(page, pageSize uint) (res *model.ServerCmdList)
res = &model.ServerCmdList{}
res.Page = int64(page)
res.PageSize = int64(pageSize)
tx := global.DB.Model(&model.ServerCmd{})
tx := DB.Model(&model.ServerCmd{})
tx.Count(&res.Total)
tx.Scopes(Paginate(page, pageSize))
tx.Find(&res.ServerCmds)
@@ -25,18 +24,18 @@ func (is *ServerCmdService) List(page, pageSize uint) (res *model.ServerCmdList)
// Info
func (is *ServerCmdService) Info(id uint) *model.ServerCmd {
u := &model.ServerCmd{}
global.DB.Where("id = ?", id).First(u)
DB.Where("id = ?", id).First(u)
return u
}
// Delete
func (is *ServerCmdService) Delete(u *model.ServerCmd) error {
return global.DB.Delete(u).Error
return DB.Delete(u).Error
}
// Create
func (is *ServerCmdService) Create(u *model.ServerCmd) error {
res := global.DB.Create(u).Error
res := DB.Create(u).Error
return res
}
@@ -45,9 +44,9 @@ func (is *ServerCmdService) SendCmd(target string, cmd string, arg string) (stri
port := 0
switch target {
case model.ServerCmdTargetIdServer:
port = global.Config.Rustdesk.IdServerPort - 1
port = Config.Rustdesk.IdServerPort - 1
case model.ServerCmdTargetRelayServer:
port = global.Config.Rustdesk.RelayServerPort
port = Config.Rustdesk.RelayServerPort
}
//组装命令
cmd = cmd + " " + arg
@@ -73,14 +72,14 @@ func (is *ServerCmdService) SendSocketCmd(ty string, port int, cmd string) (stri
}
conn, err := net.Dial(tcp, fmt.Sprintf("%s:%v", addr, port))
if err != nil {
global.Logger.Debugf("%s connect to id server failed: %v", ty, err)
Logger.Debugf("%s connect to id server failed: %v", ty, err)
return "", err
}
defer conn.Close()
//发送命令
_, err = conn.Write([]byte(cmd))
if err != nil {
global.Logger.Debugf("%s send cmd failed: %v", ty, err)
Logger.Debugf("%s send cmd failed: %v", ty, err)
return "", err
}
time.Sleep(100 * time.Millisecond)
@@ -88,12 +87,12 @@ func (is *ServerCmdService) SendSocketCmd(ty string, port int, cmd string) (stri
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil && err.Error() != "EOF" {
global.Logger.Debugf("%s read response failed: %v", ty, err)
Logger.Debugf("%s read response failed: %v", ty, err)
return "", err
}
return string(buf[:n]), nil
}
func (is *ServerCmdService) Update(f *model.ServerCmd) error {
return global.DB.Model(f).Updates(f).Error
return DB.Model(f).Updates(f).Error
}

View File

@@ -1,7 +1,11 @@
package service
import (
"github.com/lejianwen/rustdesk-api/v2/config"
"github.com/lejianwen/rustdesk-api/v2/lib/jwt"
"github.com/lejianwen/rustdesk-api/v2/lib/lock"
"github.com/lejianwen/rustdesk-api/v2/model"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
)
@@ -19,14 +23,34 @@ type Service struct {
*ShareRecordService
*ServerCmdService
*LdapService
*AppService
}
func New() *Service {
all := new(Service)
return all
type Dependencies struct {
Config *config.Config
DB *gorm.DB
Logger *log.Logger
Jwt *jwt.Jwt
Lock *lock.Locker
}
var AllService = New()
var Config *config.Config
var DB *gorm.DB
var Logger *log.Logger
var Jwt *jwt.Jwt
var Lock lock.Locker
var AllService *Service
func New(c *config.Config, g *gorm.DB, l *log.Logger, j *jwt.Jwt, lo lock.Locker) *Service {
Config = c
DB = g
Logger = l
Jwt = j
Lock = lo
AllService = new(Service)
return AllService
}
func Paginate(page, pageSize uint) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {

View File

@@ -1,7 +1,6 @@
package service
import (
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/model"
"gorm.io/gorm"
)
@@ -12,7 +11,7 @@ type ShareRecordService struct {
// InfoById 根据用户id取用户信息
func (srs *ShareRecordService) InfoById(id uint) *model.ShareRecord {
u := &model.ShareRecord{}
global.DB.Where("id = ?", id).First(u)
DB.Where("id = ?", id).First(u)
return u
}
@@ -20,7 +19,7 @@ func (srs *ShareRecordService) List(page, pageSize uint, where func(tx *gorm.DB)
res = &model.ShareRecordList{}
res.Page = int64(page)
res.PageSize = int64(pageSize)
tx := global.DB.Model(&model.ShareRecord{})
tx := DB.Model(&model.ShareRecord{})
if where != nil {
where(tx)
}
@@ -32,18 +31,18 @@ func (srs *ShareRecordService) List(page, pageSize uint, where func(tx *gorm.DB)
// Create 创建
func (srs *ShareRecordService) Create(u *model.ShareRecord) error {
res := global.DB.Create(u).Error
res := DB.Create(u).Error
return res
}
func (srs *ShareRecordService) Delete(u *model.ShareRecord) error {
return global.DB.Delete(u).Error
return DB.Delete(u).Error
}
// Update 更新
func (srs *ShareRecordService) Update(u *model.ShareRecord) error {
return global.DB.Model(u).Updates(u).Error
return DB.Model(u).Updates(u).Error
}
func (srs *ShareRecordService) BatchDelete(ids []uint) error {
return global.DB.Where("id in (?)", ids).Delete(&model.ShareRecord{}).Error
return DB.Where("id in (?)", ids).Delete(&model.ShareRecord{}).Error
}

View File

@@ -1,7 +1,6 @@
package service
import (
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/model"
"gorm.io/gorm"
)
@@ -11,12 +10,12 @@ type TagService struct {
func (s *TagService) Info(id uint) *model.Tag {
p := &model.Tag{}
global.DB.Where("id = ?", id).First(p)
DB.Where("id = ?", id).First(p)
return p
}
func (s *TagService) InfoByUserIdAndNameAndCollectionId(userid uint, name string, cid uint) *model.Tag {
p := &model.Tag{}
global.DB.Where("user_id = ? and name = ? and collection_id = ?", userid, name, cid).First(p)
DB.Where("user_id = ? and name = ? and collection_id = ?", userid, name, cid).First(p)
return p
}
@@ -34,7 +33,7 @@ func (s *TagService) ListByUserIdAndCollectionId(userId, cid uint) (res *model.T
return
}
func (s *TagService) UpdateTags(userId uint, tags map[string]uint) {
tx := global.DB.Begin()
tx := DB.Begin()
//先查询所有tag
var allTags []*model.Tag
tx.Where("user_id = ?", userId).Find(&allTags)
@@ -66,7 +65,7 @@ func (s *TagService) UpdateTags(userId uint, tags map[string]uint) {
// InfoById 根据用户id取用户信息
func (s *TagService) InfoById(id uint) *model.Tag {
u := &model.Tag{}
global.DB.Where("id = ?", id).First(u)
DB.Where("id = ?", id).First(u)
return u
}
@@ -74,7 +73,7 @@ func (s *TagService) List(page, pageSize uint, where func(tx *gorm.DB)) (res *mo
res = &model.TagList{}
res.Page = int64(page)
res.PageSize = int64(pageSize)
tx := global.DB.Model(&model.Tag{})
tx := DB.Model(&model.Tag{})
if where != nil {
where(tx)
}
@@ -86,14 +85,14 @@ func (s *TagService) List(page, pageSize uint, where func(tx *gorm.DB)) (res *mo
// Create 创建
func (s *TagService) Create(u *model.Tag) error {
res := global.DB.Create(u).Error
res := DB.Create(u).Error
return res
}
func (s *TagService) Delete(u *model.Tag) error {
return global.DB.Delete(u).Error
return DB.Delete(u).Error
}
// Update 更新
func (s *TagService) Update(u *model.Tag) error {
return global.DB.Model(u).Select("*").Omit("created_at").Updates(u).Error
return DB.Model(u).Select("*").Omit("created_at").Updates(u).Error
}

View File

@@ -2,7 +2,6 @@ package service
import (
"errors"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/utils"
"math/rand"
@@ -20,43 +19,43 @@ type UserService struct {
// InfoById 根据用户id取用户信息
func (us *UserService) InfoById(id uint) *model.User {
u := &model.User{}
global.DB.Where("id = ?", id).First(u)
DB.Where("id = ?", id).First(u)
return u
}
// InfoByUsername 根据用户名取用户信息
func (us *UserService) InfoByUsername(un string) *model.User {
u := &model.User{}
global.DB.Where("username = ?", un).First(u)
DB.Where("username = ?", un).First(u)
return u
}
// InfoByEmail 根据邮箱取用户信息
func (us *UserService) InfoByEmail(email string) *model.User {
u := &model.User{}
global.DB.Where("email = ?", email).First(u)
DB.Where("email = ?", email).First(u)
return u
}
// InfoByOpenid 根据openid取用户信息
func (us *UserService) InfoByOpenid(openid string) *model.User {
u := &model.User{}
global.DB.Where("openid = ?", openid).First(u)
DB.Where("openid = ?", openid).First(u)
return u
}
// InfoByUsernamePassword 根据用户名密码取用户信息
func (us *UserService) InfoByUsernamePassword(username, password string) *model.User {
if global.Config.Ldap.Enable {
if Config.Ldap.Enable {
u, err := AllService.LdapService.Authenticate(username, password)
if err == nil {
return u
}
global.Logger.Errorf("LDAP authentication failed, %v", err)
global.Logger.Warn("Fallback to local database")
Logger.Errorf("LDAP authentication failed, %v", err)
Logger.Warn("Fallback to local database")
}
u := &model.User{}
global.DB.Where("username = ? and password = ?", username, us.EncryptPassword(password)).First(u)
DB.Where("username = ? and password = ?", username, us.EncryptPassword(password)).First(u)
return u
}
@@ -64,21 +63,21 @@ func (us *UserService) InfoByUsernamePassword(username, password string) *model.
func (us *UserService) InfoByAccessToken(token string) (*model.User, *model.UserToken) {
u := &model.User{}
ut := &model.UserToken{}
global.DB.Where("token = ?", token).First(ut)
DB.Where("token = ?", token).First(ut)
if ut.Id == 0 {
return u, ut
}
if ut.ExpiredAt < time.Now().Unix() {
return u, ut
}
global.DB.Where("id = ?", ut.UserId).First(u)
DB.Where("id = ?", ut.UserId).First(u)
return u, ut
}
// GenerateToken 生成token
func (us *UserService) GenerateToken(u *model.User) string {
if len(global.Jwt.Key) > 0 {
return global.Jwt.GenerateToken(u.Id)
if len(Jwt.Key) > 0 {
return Jwt.GenerateToken(u.Id)
}
return utils.Md5(u.Username + time.Now().String())
}
@@ -93,9 +92,9 @@ func (us *UserService) Login(u *model.User, llog *model.LoginLog) *model.UserTok
DeviceId: llog.DeviceId,
ExpiredAt: us.UserTokenExpireTimestamp(),
}
global.DB.Create(ut)
DB.Create(ut)
llog.UserTokenId = ut.UserId
global.DB.Create(llog)
DB.Create(llog)
if llog.Uuid != "" {
AllService.PeerService.UuidBindUserId(llog.DeviceId, llog.Uuid, u.Id)
}
@@ -116,7 +115,7 @@ func (us *UserService) List(page, pageSize uint, where func(tx *gorm.DB)) (res *
res = &model.UserList{}
res.Page = int64(page)
res.PageSize = int64(pageSize)
tx := global.DB.Model(&model.User{})
tx := DB.Model(&model.User{})
if where != nil {
where(tx)
}
@@ -127,7 +126,7 @@ func (us *UserService) List(page, pageSize uint, where func(tx *gorm.DB)) (res *
}
func (us *UserService) ListByIds(ids []uint) (res []*model.User) {
global.DB.Where("id in ?", ids).Find(&res)
DB.Where("id in ?", ids).Find(&res)
return res
}
@@ -141,14 +140,14 @@ func (us *UserService) ListByGroupId(groupId, page, pageSize uint) (res *model.U
// ListIdsByGroupId 根据组id取用户id列表
func (us *UserService) ListIdsByGroupId(groupId uint) (ids []uint) {
global.DB.Model(&model.User{}).Where("group_id = ?", groupId).Pluck("id", &ids)
DB.Model(&model.User{}).Where("group_id = ?", groupId).Pluck("id", &ids)
return ids
}
// ListIdAndNameByGroupId 根据组id取用户id和用户名列表
func (us *UserService) ListIdAndNameByGroupId(groupId uint) (res []*model.User) {
global.DB.Model(&model.User{}).Where("group_id = ?", groupId).Select("id, username").Find(&res)
DB.Model(&model.User{}).Where("group_id = ?", groupId).Select("id, username").Find(&res)
return res
}
@@ -170,14 +169,14 @@ func (us *UserService) Create(u *model.User) error {
}
u.Username = us.formatUsername(u.Username)
u.Password = us.EncryptPassword(u.Password)
res := global.DB.Create(u).Error
res := DB.Create(u).Error
return res
}
// GetUuidByToken 根据token和user取uuid
func (us *UserService) GetUuidByToken(u *model.User, token string) string {
ut := &model.UserToken{}
err := global.DB.Where("user_id = ? and token = ?", u.Id, token).First(ut).Error
err := DB.Where("user_id = ? and token = ?", u.Id, token).First(ut).Error
if err != nil {
return ""
}
@@ -187,7 +186,7 @@ func (us *UserService) GetUuidByToken(u *model.User, token string) string {
// Logout 退出登录 -> 删除token, 解绑uuid
func (us *UserService) Logout(u *model.User, token string) error {
uuid := us.GetUuidByToken(u, token)
err := global.DB.Where("user_id = ? and token = ?", u.Id, token).Delete(&model.UserToken{}).Error
err := DB.Where("user_id = ? and token = ?", u.Id, token).Delete(&model.UserToken{}).Error
if err != nil {
return err
}
@@ -203,7 +202,7 @@ func (us *UserService) Delete(u *model.User) error {
if userCount <= 1 && us.IsAdmin(u) {
return errors.New("The last admin user cannot be deleted")
}
tx := global.DB.Begin()
tx := DB.Begin()
// 删除用户
if err := tx.Delete(u).Error; err != nil {
tx.Rollback()
@@ -232,7 +231,7 @@ func (us *UserService) Delete(u *model.User) error {
tx.Commit()
// 删除关联的peer
if err := AllService.PeerService.EraseUserId(u.Id); err != nil {
global.Logger.Warn("User deleted successfully, but failed to unlink peer.")
Logger.Warn("User deleted successfully, but failed to unlink peer.")
return nil
}
return nil
@@ -249,28 +248,28 @@ func (us *UserService) Update(u *model.User) error {
return errors.New("The last admin user cannot be disabled or demoted")
}
}
return global.DB.Model(u).Updates(u).Error
return DB.Model(u).Updates(u).Error
}
// FlushToken 清空token
func (us *UserService) FlushToken(u *model.User) error {
return global.DB.Where("user_id = ?", u.Id).Delete(&model.UserToken{}).Error
return DB.Where("user_id = ?", u.Id).Delete(&model.UserToken{}).Error
}
// FlushTokenByUuid 清空token
func (us *UserService) FlushTokenByUuid(uuid string) error {
return global.DB.Where("device_uuid = ?", uuid).Delete(&model.UserToken{}).Error
return DB.Where("device_uuid = ?", uuid).Delete(&model.UserToken{}).Error
}
// FlushTokenByUuids 清空token
func (us *UserService) FlushTokenByUuids(uuids []string) error {
return global.DB.Where("device_uuid in (?)", uuids).Delete(&model.UserToken{}).Error
return DB.Where("device_uuid in (?)", uuids).Delete(&model.UserToken{}).Error
}
// UpdatePassword 更新密码
func (us *UserService) UpdatePassword(u *model.User, password string) error {
u.Password = us.EncryptPassword(password)
err := global.DB.Model(u).Update("password", u.Password).Error
err := DB.Model(u).Update("password", u.Password).Error
if err != nil {
return err
}
@@ -306,8 +305,8 @@ func (us *UserService) InfoByOauthId(op string, openId string) *model.User {
// RegisterByOauth 注册
func (us *UserService) RegisterByOauth(oauthUser *model.OauthUser, op string) (error, *model.User) {
global.Lock.Lock("registerByOauth")
defer global.Lock.UnLock("registerByOauth")
Lock.Lock("registerByOauth")
defer Lock.UnLock("registerByOauth")
ut := AllService.OauthService.UserThirdInfo(op, oauthUser.OpenId)
if ut.Id != 0 {
return nil, us.InfoById(ut.UserId)
@@ -335,12 +334,12 @@ func (us *UserService) RegisterByOauth(oauthUser *model.OauthUser, op string) (e
}
if user.Id != 0 {
ut.FromOauthUser(user.Id, oauthUser, oauthType, op)
global.DB.Create(ut)
DB.Create(ut)
return nil, user
}
}
tx := global.DB.Begin()
tx := DB.Begin()
ut = &model.UserThird{}
ut.FromOauthUser(0, oauthUser, oauthType, op)
// The initial username should be formatted
@@ -372,27 +371,27 @@ func (us *UserService) GenerateUsernameByOauth(name string) string {
// UserThirdsByUserId
func (us *UserService) UserThirdsByUserId(userId uint) (res []*model.UserThird) {
global.DB.Where("user_id = ?", userId).Find(&res)
DB.Where("user_id = ?", userId).Find(&res)
return res
}
func (us *UserService) UserThirdInfo(userId uint, op string) *model.UserThird {
ut := &model.UserThird{}
global.DB.Where("user_id = ? and op = ?", userId, op).First(ut)
DB.Where("user_id = ? and op = ?", userId, op).First(ut)
return ut
}
// FindLatestUserIdFromLoginLogByUuid 根据uuid查找最后登录的用户id
func (us *UserService) FindLatestUserIdFromLoginLogByUuid(uuid string) uint {
llog := &model.LoginLog{}
global.DB.Where("uuid = ?", uuid).Order("id desc").First(llog)
DB.Where("uuid = ?", uuid).Order("id desc").First(llog)
return llog.UserId
}
// IsPasswordEmptyById 根据用户id判断密码是否为空主要用于第三方登录的自动注册
func (us *UserService) IsPasswordEmptyById(id uint) bool {
u := &model.User{}
if global.DB.Where("id = ?", id).First(u).Error != nil {
if DB.Where("id = ?", id).First(u).Error != nil {
return false
}
return u.Password == ""
@@ -401,7 +400,7 @@ func (us *UserService) IsPasswordEmptyById(id uint) bool {
// IsPasswordEmptyByUsername 根据用户id判断密码是否为空主要用于第三方登录的自动注册
func (us *UserService) IsPasswordEmptyByUsername(username string) bool {
u := &model.User{}
if global.DB.Where("username = ?", username).First(u).Error != nil {
if DB.Where("username = ?", username).First(u).Error != nil {
return false
}
return u.Password == ""
@@ -431,7 +430,7 @@ func (us *UserService) TokenList(page uint, size uint, f func(tx *gorm.DB)) *mod
res := &model.UserTokenList{}
res.Page = int64(page)
res.PageSize = int64(size)
tx := global.DB.Model(&model.UserToken{})
tx := DB.Model(&model.UserToken{})
if f != nil {
f(tx)
}
@@ -443,12 +442,12 @@ func (us *UserService) TokenList(page uint, size uint, f func(tx *gorm.DB)) *mod
func (us *UserService) TokenInfoById(id uint) *model.UserToken {
ut := &model.UserToken{}
global.DB.Where("id = ?", id).First(ut)
DB.Where("id = ?", id).First(ut)
return ut
}
func (us *UserService) DeleteToken(l *model.UserToken) error {
return global.DB.Delete(l).Error
return DB.Delete(l).Error
}
// Helper functions, used for formatting username
@@ -461,29 +460,30 @@ func (us *UserService) formatUsername(username string) string {
// Helper functions, getUserCount
func (us *UserService) getUserCount() int64 {
var count int64
global.DB.Model(&model.User{}).Count(&count)
DB.Model(&model.User{}).Count(&count)
return count
}
// helper functions, getAdminUserCount
func (us *UserService) getAdminUserCount() int64 {
var count int64
global.DB.Model(&model.User{}).Where("is_admin = ?", true).Count(&count)
DB.Model(&model.User{}).Where("is_admin = ?", true).Count(&count)
return count
}
// UserTokenExpireTimestamp 生成用户token过期时间
func (us *UserService) UserTokenExpireTimestamp() int64 {
exp := global.Config.App.TokenExpire
exp := Config.App.TokenExpire
if exp == 0 {
exp = 3600 * 24 * 7
//默认七天
exp = 604800
}
return time.Now().Add(time.Second * time.Duration(exp)).Unix()
return time.Now().Add(exp).Unix()
}
func (us *UserService) RefreshAccessToken(ut *model.UserToken) {
ut.ExpiredAt = us.UserTokenExpireTimestamp()
global.DB.Model(ut).Update("expired_at", ut.ExpiredAt)
DB.Model(ut).Update("expired_at", ut.ExpiredAt)
}
func (us *UserService) AutoRefreshAccessToken(ut *model.UserToken) {
if ut.ExpiredAt-time.Now().Unix() < 86400 {
@@ -492,11 +492,11 @@ func (us *UserService) AutoRefreshAccessToken(ut *model.UserToken) {
}
func (us *UserService) BatchDeleteUserToken(ids []uint) error {
return global.DB.Where("id in ?", ids).Delete(&model.UserToken{}).Error
return DB.Where("id in ?", ids).Delete(&model.UserToken{}).Error
}
func (us *UserService) VerifyJWT(token string) (uint, error) {
return global.Jwt.ParseToken(token)
return Jwt.ParseToken(token)
}
// IsUsernameExists 判断用户名是否存在, it will check the internal database and LDAP(if enabled)
@@ -506,7 +506,7 @@ func (us *UserService) IsUsernameExists(username string) bool {
func (us *UserService) IsUsernameExistsLocal(username string) bool {
u := &model.User{}
global.DB.Where("username = ?", username).First(u)
DB.Where("username = ?", username).First(u)
return u.Id != 0
}

View File

@@ -7,6 +7,7 @@ import (
"math/rand"
"reflect"
"runtime/debug"
"strings"
)
func Md5(str string) string {
@@ -100,3 +101,11 @@ func InArray(k string, arr []string) bool {
}
return false
}
func StringConcat(strs ...string) string {
var builder strings.Builder
for _, str := range strs {
builder.WriteString(str)
}
return builder.String()
}