Compare commits

...

21 Commits

Author SHA1 Message Date
lejianwen
9d08c61390 fix: Normal user can not change the name of their own address book (#341) 2025-08-10 11:17:51 +08:00
Tao Chen
6f092472b1 feat: Optimize login workflow (#345)
* add "disable_pwd" and "auto_oidc" at /admin/login-options

* fix: build RedirectURL by host and scheme, not Origin
2025-07-31 10:46:11 +08:00
caicob
4876746f7a docs: README_EN.md (#340) 2025-07-31 10:42:56 +08:00
startgo
05d2d1642a feat: Update zh_TW.toml (#322)
Corrected translation to match Taiwanese Traditional Chinese usage
2025-07-19 21:08:40 +08:00
lejianwen
59fdd6424b feat(captcha): The captcha generates uppercase letter images, but it can only recognize them as lowercase (#319) 2025-07-14 20:36:33 +08:00
lejianwen
0feee5115f fix: Oauth callback url is fixed to host+/api/oidc/callback (#314) 2025-07-11 09:55:48 +08:00
Plynksiy Nikita
65f0a9e3cf docs: add note about requiring conf and resources directories (or specifying paths via -c and RUSTDESK_API_GIN_RESOURCES_PATH) when running without docker (#311) 2025-07-09 09:43:56 +08:00
lejianwen
77836a4e56 feat(user): add remark field to User model and form (#307) 2025-07-08 12:14:46 +08:00
lejianwen
09f8316bf1 feat: Update database version constant to 264 2025-07-08 12:13:31 +08:00
k3-cat
c52706e621 feat: Improve oauth redirect (#303)
* fix: redirects after oauth can potentially misalign with server's actually hostname

* feat: remove `RedirectURL` from oauth config, as it should checked by provider rather than client

* feat: align oauth endpoint with the hostname in requests
2025-07-04 14:30:07 +08:00
k3-cat
17dcff4f43 feat: better autorenewal (#305) 2025-07-04 14:27:47 +08:00
Plynksiy Nikita
0b39c4e104 feat(password): Password hashing with bcrypt (#290)
* feat(password): add configurable password hashing with md5 and bcrypt

* docs: add password hashing algorithm configuration (bcrypt/md5)

* feat(password): better bcrypt fallback and minor refactoring

* feat(password): handle errors in password encryption and verification

* feat(password): remove password hashing algorithm configuration
2025-06-24 17:23:36 +08:00
Plynksiy Nikita
ee176b314e feat: Use crypto/rand for secure random string generation (#293) 2025-06-18 20:47:24 +08:00
Plynksiy Nikita
1ffc9c4a5b fix: correct typo in build arg FRONTEND_GIT_REPO (#292) 2025-06-18 20:42:13 +08:00
Plynksiy Nikita
1257246552 feat(i18n): replace hardcoded messages with translated strings (#289) 2025-06-17 09:05:10 +08:00
lejianwen
2948eaaa5c chore: Update Go version to 1.23 in build configurations 2025-06-16 15:41:16 +08:00
lejianwen
8641ba5c0c docs: Update swagger docs 2025-06-16 12:31:48 +08:00
lejianwen
60b7a18fe7 feat: Add PostgreSQL support and refactor MySQL DSN handling (#284) 2025-06-16 12:26:08 +08:00
lejianwen
ca068816ae feat: Add start time in /api/sysinfover 2025-06-16 12:23:48 +08:00
lejianwen
06648d9a6c fix(admin): Use admin-hello first
(#274) (#255)
2025-06-15 15:33:12 +08:00
puyujian
8a8abd5163 feat(oauth): 支持linux.do登录 (#280)
* 支持linux.do登录

* 修正
2025-06-15 15:32:20 +08:00
53 changed files with 736 additions and 293 deletions

View File

@@ -66,7 +66,7 @@ jobs:
- name: Set up Go environment - name: Set up Go environment
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: '1.22' # 选择 Go 版本 go-version: '1.23' # 选择 Go 版本
- name: Set up npm - name: Set up npm
uses: actions/setup-node@v2 uses: actions/setup-node@v2
@@ -147,6 +147,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate Changelog - name: Generate Changelog
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
run: npx changelogithub # or changelogithub@0.12 if ensure the stable result run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
env: env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

View File

@@ -61,7 +61,7 @@ jobs:
- name: Set up Go environment - name: Set up Go environment
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: '1.22' # 选择 Go 版本 go-version: '1.23' # 选择 Go 版本
- name: Set up npm - name: Set up npm
uses: actions/setup-node@v2 uses: actions/setup-node@v2

View File

@@ -42,11 +42,11 @@ RUN if [ "$COUNTRY" = "CN" ] ; then \
fi && \ fi && \
apk update && apk add --no-cache git apk update && apk add --no-cache git
ARG FREONTEND_GIT_REPO=https://github.com/lejianwen/rustdesk-api-web.git ARG FRONTEND_GIT_REPO=https://github.com/lejianwen/rustdesk-api-web.git
ARG FRONTEND_GIT_BRANCH=master ARG FRONTEND_GIT_BRANCH=master
# Clone the frontend repository # Clone the frontend repository
RUN git clone -b $FRONTEND_GIT_BRANCH $FREONTEND_GIT_REPO . RUN git clone -b $FRONTEND_GIT_BRANCH $FRONTEND_GIT_REPO .
# Install required tools without caching index to minimize image size # Install required tools without caching index to minimize image size
RUN if [ "$COUNTRY" = "CN" ] ; then \ RUN if [ "$COUNTRY" = "CN" ] ; then \

View File

@@ -94,8 +94,8 @@
- 对于`OIDC`, `Issuer`是必须的。`Scopes`是可选的,默认为 `openid,profile,email`. 确保可以获取 `sub`,`email``preferred_username` - 对于`OIDC`, `Issuer`是必须的。`Scopes`是可选的,默认为 `openid,profile,email`. 确保可以获取 `sub`,`email``preferred_username`
- `github oauth app``Settings`->`Developer settings`->`OAuth Apps`->`New OAuth App` - `github oauth app``Settings`->`Developer settings`->`OAuth Apps`->`New OAuth App`
中创建,地址 [https://github.com/settings/developers](https://github.com/settings/developers) 中创建,地址 [https://github.com/settings/developers](https://github.com/settings/developers)
- `Authorization callback URL`填写`http://<your server[:port]>/api/oauth/callback` - `Authorization callback URL`填写`http://<your server[:port]>/api/oidc/callback`
,比如`http://127.0.0.1:21114/api/oauth/callback` ,比如`http://127.0.0.1:21114/api/oidc/callback`
7. 登录日志 7. 登录日志
8. 链接日志 8. 链接日志
9. 文件传输日志 9. 文件传输日志
@@ -255,6 +255,12 @@
#或者使用generate_api.go生成api并运行 #或者使用generate_api.go生成api并运行
go generate generate_api.go go generate generate_api.go
``` ```
> 注意:使用 `go run` 或编译后的二进制时,当前目录下必须存在 `conf` 和 `resources`
> 目录。如果在其他目录运行,可通过 `-c` 和环境变量
> `RUSTDESK_API_GIN_RESOURCES_PATH` 指定绝对路径,例如:
> ```bash
> RUSTDESK_API_GIN_RESOURCES_PATH=/opt/rustdesk-api/resources ./apimain -c /opt/rustdesk-api/conf/config.yaml
> ```
5. 编译,如果想自己编译,先cd到项目根目录然后windows下直接运行`build.bat`,linux下运行`build.sh`,编译后会在`release` 5. 编译,如果想自己编译,先cd到项目根目录然后windows下直接运行`build.bat`,linux下运行`build.sh`,编译后会在`release`
目录下生成对应的可执行文件。直接运行编译后的可执行文件即可。 目录下生成对应的可执行文件。直接运行编译后的可执行文件即可。

View File

@@ -94,8 +94,8 @@ displaying data.Frontend code is available at [rustdesk-api-web](https://github.
- For `OIDC`, you must set the `Issuer`. And `Scopes` is optional which default is `openid,email,profile`, please make sure this `Oauth App` can access `sub`, `email` and `preferred_username` - For `OIDC`, you must set the `Issuer`. And `Scopes` is optional which default is `openid,email,profile`, please make sure this `Oauth App` can access `sub`, `email` and `preferred_username`
- Create a `GitHub OAuth App` - Create a `GitHub OAuth App`
at `Settings` -> `Developer settings` -> `OAuth Apps` -> `New OAuth App` [here](https://github.com/settings/developers). at `Settings` -> `Developer settings` -> `OAuth Apps` -> `New OAuth App` [here](https://github.com/settings/developers).
- Set the `Authorization callback URL` to `http://<your server[:port]>/api/oauth/callback`, - Set the `Authorization callback URL` to `http://<your server[:port]>/api/oidc/callback`,
e.g., `http://127.0.0.1:21114/api/oauth/callback`. e.g., `http://127.0.0.1:21114/api/oidc/callback`.
7. Login logs 7. Login logs
8. Connection logs 8. Connection logs
@@ -255,6 +255,13 @@ Download the release from [release](https://github.com/lejianwen/rustdesk-api/re
# Or generate and run the API using generate_api.go # Or generate and run the API using generate_api.go
go generate generate_api.go go generate generate_api.go
``` ```
> **Note:** When using `go run` or the compiled binary, the `conf` and `resources`
> directories must exist relative to the current working directory. If you run
> the program from another location, specify absolute paths with `-c` and the
> `RUSTDESK_API_GIN_RESOURCES_PATH` environment variable. Example:
> ```bash
> RUSTDESK_API_GIN_RESOURCES_PATH=/opt/rustdesk-api/resources ./apimain -c /opt/rustdesk-api/conf/config.yaml
> ```
5. To compile, change to the project root directory. For Windows, run `build.bat`, and for Linux, run `build.sh`. After 5. To compile, change to the project root directory. For Windows, run `build.bat`, and for Linux, run `build.sh`. After
compiling, the corresponding executables will be generated in the `release` directory. Run the compiled executables compiling, the corresponding executables will be generated in the `release` directory. Run the compiled executables

View File

@@ -1,6 +1,11 @@
package main package main
import ( import (
"fmt"
"os"
"strconv"
"time"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/lejianwen/rustdesk-api/v2/config" "github.com/lejianwen/rustdesk-api/v2/config"
"github.com/lejianwen/rustdesk-api/v2/global" "github.com/lejianwen/rustdesk-api/v2/global"
@@ -16,11 +21,10 @@ import (
"github.com/lejianwen/rustdesk-api/v2/utils" "github.com/lejianwen/rustdesk-api/v2/utils"
"github.com/nicksnyder/go-i18n/v2/i18n" "github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"os"
"strconv"
"time"
) )
const DatabaseVersion = 264
// @title 管理系统API // @title 管理系统API
// @version 1.0 // @version 1.0
// @description 接口 // @description 接口
@@ -140,18 +144,40 @@ func InitGlobal() {
} }
//gorm //gorm
if global.Config.Gorm.Type == config.TypeMysql { if global.Config.Gorm.Type == config.TypeMysql {
dns := global.Config.Mysql.Username + ":" + global.Config.Mysql.Password + "@(" + global.Config.Mysql.Addr + ")/" + global.Config.Mysql.Dbname + "?charset=utf8mb4&parseTime=True&loc=Local"
dsn := fmt.Sprintf("%s:%s@(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
global.Config.Mysql.Username,
global.Config.Mysql.Password,
global.Config.Mysql.Addr,
global.Config.Mysql.Dbname,
)
global.DB = orm.NewMysql(&orm.MysqlConfig{ global.DB = orm.NewMysql(&orm.MysqlConfig{
Dns: dns, Dsn: dsn,
MaxIdleConns: global.Config.Gorm.MaxIdleConns, MaxIdleConns: global.Config.Gorm.MaxIdleConns,
MaxOpenConns: global.Config.Gorm.MaxOpenConns, MaxOpenConns: global.Config.Gorm.MaxOpenConns,
}) }, global.Logger)
} else if global.Config.Gorm.Type == config.TypePostgresql {
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s TimeZone=%s",
global.Config.Postgresql.Host,
global.Config.Postgresql.Port,
global.Config.Postgresql.User,
global.Config.Postgresql.Password,
global.Config.Postgresql.Dbname,
global.Config.Postgresql.Sslmode,
global.Config.Postgresql.TimeZone,
)
global.DB = orm.NewPostgresql(&orm.PostgresqlConfig{
Dsn: dsn,
MaxIdleConns: global.Config.Gorm.MaxIdleConns,
MaxOpenConns: global.Config.Gorm.MaxOpenConns,
}, global.Logger)
} else { } else {
//sqlite //sqlite
global.DB = orm.NewSqlite(&orm.SqliteConfig{ global.DB = orm.NewSqlite(&orm.SqliteConfig{
MaxIdleConns: global.Config.Gorm.MaxIdleConns, MaxIdleConns: global.Config.Gorm.MaxIdleConns,
MaxOpenConns: global.Config.Gorm.MaxOpenConns, MaxOpenConns: global.Config.Gorm.MaxOpenConns,
}) }, global.Logger)
} }
//validator //validator
@@ -187,7 +213,7 @@ func InitGlobal() {
} }
func DatabaseAutoUpdate() { func DatabaseAutoUpdate() {
version := 262 version := DatabaseVersion
db := global.DB db := global.DB
@@ -197,11 +223,17 @@ func DatabaseAutoUpdate() {
if dbName == "" { if dbName == "" {
dbName = global.Config.Mysql.Dbname dbName = global.Config.Mysql.Dbname
// 移除 DSN 中的数据库名称,以便初始连接时不指定数据库 // 移除 DSN 中的数据库名称,以便初始连接时不指定数据库
dsnWithoutDB := global.Config.Mysql.Username + ":" + global.Config.Mysql.Password + "@(" + global.Config.Mysql.Addr + ")/?charset=utf8mb4&parseTime=True&loc=Local" dsnWithoutDB := fmt.Sprintf("%s:%s@(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
global.Config.Mysql.Username,
global.Config.Mysql.Password,
global.Config.Mysql.Addr,
"",
)
//新链接 //新链接
dbWithoutDB := orm.NewMysql(&orm.MysqlConfig{ dbWithoutDB := orm.NewMysql(&orm.MysqlConfig{
Dns: dsnWithoutDB, Dsn: dsnWithoutDB,
}) }, global.Logger)
// 获取底层的 *sql.DB 对象,并确保在程序退出时关闭连接 // 获取底层的 *sql.DB 对象,并确保在程序退出时关闭连接
sqlDBWithoutDB, err := dbWithoutDB.DB() sqlDBWithoutDB, err := dbWithoutDB.DB()
if err != nil { if err != nil {
@@ -313,7 +345,11 @@ func Migrate(version uint) {
// 生成随机密码 // 生成随机密码
pwd := utils.RandomString(8) pwd := utils.RandomString(8)
global.Logger.Info("Admin Password Is: ", pwd) global.Logger.Info("Admin Password Is: ", pwd)
admin.Password = service.AllService.UserService.EncryptPassword(pwd) var err error
admin.Password, err = utils.EncryptPassword(pwd)
if err != nil {
global.Logger.Fatalf("failed to generate admin password: %v", err)
}
global.DB.Create(admin) global.DB.Create(admin)
} }

View File

@@ -11,7 +11,7 @@ app:
disable-pwd-login: false #禁用密码登录 disable-pwd-login: false #禁用密码登录
admin: admin:
title: "RustDesk Api Admin" title: "RustDesk API Admin"
hello-file: "./conf/admin/hello.html" #优先使用file hello-file: "./conf/admin/hello.html" #优先使用file
hello: "" hello: ""
# ID Server and Relay Server ports https://github.com/lejianwen/rustdesk-api/issues/257 # ID Server and Relay Server ports https://github.com/lejianwen/rustdesk-api/issues/257
@@ -31,6 +31,16 @@ mysql:
password: "" password: ""
addr: "" addr: ""
dbname: "" dbname: ""
postgresql:
host: "127.0.0.1"
port: "5432"
user: ""
password: ""
dbname: "postgres"
sslmode: "disable" # disable, require, verify-ca, verify-full
time-zone: "Asia/Shanghai" # Time zone for PostgreSQL connection
rustdesk: rustdesk:
id-server: "192.168.1.66:21116" id-server: "192.168.1.66:21116"
relay-server: "192.168.1.66:21117" relay-server: "192.168.1.66:21117"

View File

@@ -37,6 +37,7 @@ type Config struct {
Admin Admin Admin Admin
Gorm Gorm Gorm Gorm
Mysql Mysql Mysql Mysql
Postgresql Postgresql
Gin Gin Gin Gin
Logger Logger Logger Logger
Redis Redis Redis Redis

View File

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

View File

@@ -3,18 +3,20 @@ package config
type GithubOauth struct { type GithubOauth struct {
ClientId string `mapstructure:"client-id"` ClientId string `mapstructure:"client-id"`
ClientSecret string `mapstructure:"client-secret"` ClientSecret string `mapstructure:"client-secret"`
RedirectUrl string `mapstructure:"redirect-url"`
} }
type GoogleOauth struct { type GoogleOauth struct {
ClientId string `mapstructure:"client-id"` ClientId string `mapstructure:"client-id"`
ClientSecret string `mapstructure:"client-secret"` ClientSecret string `mapstructure:"client-secret"`
RedirectUrl string `mapstructure:"redirect-url"`
} }
type OidcOauth struct { type OidcOauth struct {
Issuer string `mapstructure:"issuer"` Issuer string `mapstructure:"issuer"`
ClientId string `mapstructure:"client-id"` ClientId string `mapstructure:"client-id"`
ClientSecret string `mapstructure:"client-secret"` ClientSecret string `mapstructure:"client-secret"`
RedirectUrl string `mapstructure:"redirect-url"` }
type LinuxdoOauth struct {
ClientId string `mapstructure:"client-id"`
ClientSecret string `mapstructure:"client-secret"`
} }

View File

@@ -5,7 +5,7 @@ services:
dockerfile: Dockerfile.dev dockerfile: Dockerfile.dev
args: args:
COUNTRY: CN COUNTRY: CN
FREONTEND_GIT_REPO: https://github.com/lejianwen/rustdesk-api-web.git FRONTEND_GIT_REPO: https://github.com/lejianwen/rustdesk-api-web.git
FRONTEND_GIT_BRANCH: master FRONTEND_GIT_BRANCH: master
# image: lejianwen/rustdesk-api # image: lejianwen/rustdesk-api
container_name: rustdesk-api container_name: rustdesk-api

View File

@@ -1,4 +1,4 @@
// Package admin Content generated by swaggo/swag. DO NOT EDIT // Package admin Code generated by swaggo/swag. DO NOT EDIT
package admin package admin
import "github.com/swaggo/swag" import "github.com/swaggo/swag"
@@ -5569,8 +5569,7 @@ const docTemplateadmin = `{
"required": [ "required": [
"client_id", "client_id",
"client_secret", "client_secret",
"oauth_type", "oauth_type"
"redirect_url"
], ],
"properties": { "properties": {
"auto_register": { "auto_register": {
@@ -5600,9 +5599,6 @@ const docTemplateadmin = `{
"pkce_method": { "pkce_method": {
"type": "string" "type": "string"
}, },
"redirect_url": {
"type": "string"
},
"scopes": { "scopes": {
"type": "string" "type": "string"
} }
@@ -5828,6 +5824,9 @@ const docTemplateadmin = `{
"captcha": { "captcha": {
"type": "string" "type": "string"
}, },
"captcha_id": {
"type": "string"
},
"password": { "password": {
"type": "string" "type": "string"
}, },
@@ -6293,9 +6292,6 @@ const docTemplateadmin = `{
"pkce_method": { "pkce_method": {
"type": "string" "type": "string"
}, },
"redirect_url": {
"type": "string"
},
"scopes": { "scopes": {
"type": "string" "type": "string"
}, },

View File

@@ -5562,8 +5562,7 @@
"required": [ "required": [
"client_id", "client_id",
"client_secret", "client_secret",
"oauth_type", "oauth_type"
"redirect_url"
], ],
"properties": { "properties": {
"auto_register": { "auto_register": {
@@ -5593,9 +5592,6 @@
"pkce_method": { "pkce_method": {
"type": "string" "type": "string"
}, },
"redirect_url": {
"type": "string"
},
"scopes": { "scopes": {
"type": "string" "type": "string"
} }
@@ -5821,6 +5817,9 @@
"captcha": { "captcha": {
"type": "string" "type": "string"
}, },
"captcha_id": {
"type": "string"
},
"password": { "password": {
"type": "string" "type": "string"
}, },
@@ -6286,9 +6285,6 @@
"pkce_method": { "pkce_method": {
"type": "string" "type": "string"
}, },
"redirect_url": {
"type": "string"
},
"scopes": { "scopes": {
"type": "string" "type": "string"
}, },

View File

@@ -143,15 +143,12 @@ definitions:
type: boolean type: boolean
pkce_method: pkce_method:
type: string type: string
redirect_url:
type: string
scopes: scopes:
type: string type: string
required: required:
- client_id - client_id
- client_secret - client_secret
- oauth_type - oauth_type
- redirect_url
type: object type: object
admin.PeerBatchDeleteForm: admin.PeerBatchDeleteForm:
properties: properties:
@@ -297,6 +294,8 @@ definitions:
properties: properties:
captcha: captcha:
type: string type: string
captcha_id:
type: string
password: password:
type: string type: string
platform: platform:
@@ -609,8 +608,6 @@ definitions:
type: boolean type: boolean
pkce_method: pkce_method:
type: string type: string
redirect_url:
type: string
scopes: scopes:
type: string type: string
updated_at: updated_at:

View File

@@ -1,4 +1,4 @@
// Package api Content generated by swaggo/swag. DO NOT EDIT // Package api Code generated by swaggo/swag. DO NOT EDIT
package api package api
import "github.com/swaggo/swag" import "github.com/swaggo/swag"
@@ -954,35 +954,6 @@ const docTemplateapi = `{
} }
} }
}, },
"/oauth/callback": {
"get": {
"description": "OauthCallback",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Oauth"
],
"summary": "OauthCallback",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.LoginRes"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/oidc/auth": { "/oidc/auth": {
"post": { "post": {
"description": "OidcAuth", "description": "OidcAuth",
@@ -1041,6 +1012,35 @@ const docTemplateapi = `{
} }
} }
}, },
"/oidc/callback": {
"get": {
"description": "OauthCallback",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Oauth"
],
"summary": "OauthCallback",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.LoginRes"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/peers": { "/peers": {
"get": { "get": {
"security": [ "security": [
@@ -1208,7 +1208,7 @@ const docTemplateapi = `{
"application/json" "application/json"
], ],
"tags": [ "tags": [
"地址" "System"
], ],
"summary": "提交系统信息", "summary": "提交系统信息",
"parameters": [ "parameters": [
@@ -1238,6 +1238,35 @@ const docTemplateapi = `{
} }
} }
}, },
"/sysinfo_ver": {
"post": {
"description": "获取系统版本信息",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"System"
],
"summary": "获取系统版本信息",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/users": { "/users": {
"get": { "get": {
"security": [ "security": [

View File

@@ -947,35 +947,6 @@
} }
} }
}, },
"/oauth/callback": {
"get": {
"description": "OauthCallback",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Oauth"
],
"summary": "OauthCallback",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.LoginRes"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/oidc/auth": { "/oidc/auth": {
"post": { "post": {
"description": "OidcAuth", "description": "OidcAuth",
@@ -1034,6 +1005,35 @@
} }
} }
}, },
"/oidc/callback": {
"get": {
"description": "OauthCallback",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Oauth"
],
"summary": "OauthCallback",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.LoginRes"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/peers": { "/peers": {
"get": { "get": {
"security": [ "security": [
@@ -1201,7 +1201,7 @@
"application/json" "application/json"
], ],
"tags": [ "tags": [
"地址" "System"
], ],
"summary": "提交系统信息", "summary": "提交系统信息",
"parameters": [ "parameters": [
@@ -1231,6 +1231,35 @@
} }
} }
}, },
"/sysinfo_ver": {
"post": {
"description": "获取系统版本信息",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"System"
],
"summary": "获取系统版本信息",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/users": { "/users": {
"get": { "get": {
"security": [ "security": [

View File

@@ -792,25 +792,6 @@ paths:
summary: 登出 summary: 登出
tags: tags:
- 登录 - 登录
/oauth/callback:
get:
consumes:
- application/json
description: OauthCallback
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.LoginRes'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: OauthCallback
tags:
- Oauth
/oidc/auth: /oidc/auth:
post: post:
consumes: consumes:
@@ -849,6 +830,25 @@ paths:
summary: OidcAuthQuery summary: OidcAuthQuery
tags: tags:
- Oauth - Oauth
/oidc/callback:
get:
consumes:
- application/json
description: OauthCallback
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.LoginRes'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: OauthCallback
tags:
- Oauth
/peers: /peers:
get: get:
consumes: consumes:
@@ -973,7 +973,26 @@ paths:
$ref: '#/definitions/response.ErrorResponse' $ref: '#/definitions/response.ErrorResponse'
summary: 提交系统信息 summary: 提交系统信息
tags: tags:
- 地址 - System
/sysinfo_ver:
post:
consumes:
- application/json
description: 获取系统版本信息
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: 获取系统版本信息
tags:
- System
/users: /users:
get: get:
consumes: consumes:

23
go.mod
View File

@@ -1,19 +1,23 @@
module github.com/lejianwen/rustdesk-api/v2 module github.com/lejianwen/rustdesk-api/v2
go 1.22 go 1.23
toolchain go1.23.10
require ( require (
github.com/BurntSushi/toml v1.3.2 github.com/BurntSushi/toml v1.3.2
github.com/antonfisher/nested-logrus-formatter v1.3.1 github.com/antonfisher/nested-logrus-formatter v1.3.1
github.com/fsnotify/fsnotify v1.5.1 github.com/coreos/go-oidc/v3 v3.12.0
github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6
github.com/gin-gonic/gin v1.9.0 github.com/gin-gonic/gin v1.9.0
github.com/go-ldap/ldap/v3 v3.4.10
github.com/go-playground/locales v0.14.1 github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1 github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.26.0 github.com/go-playground/validator/v10 v10.26.0
github.com/go-redis/redis/v8 v8.11.4 github.com/go-redis/redis/v8 v8.11.4
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/mojocn/base64Captcha v1.3.6
github.com/nicksnyder/go-i18n/v2 v2.4.0 github.com/nicksnyder/go-i18n/v2 v2.4.0
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.8.1 github.com/spf13/cobra v1.8.1
@@ -24,8 +28,9 @@ require (
golang.org/x/oauth2 v0.23.0 golang.org/x/oauth2 v0.23.0
golang.org/x/text v0.22.0 golang.org/x/text v0.22.0
gorm.io/driver/mysql v1.5.7 gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlite v1.5.6 gorm.io/driver/sqlite v1.5.6
gorm.io/gorm v1.25.7 gorm.io/gorm v1.25.10
) )
require ( require (
@@ -36,13 +41,12 @@ require (
github.com/bytedance/sonic v1.8.0 // indirect github.com/bytedance/sonic v1.8.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // 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-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/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect github.com/go-openapi/spec v0.20.4 // indirect
@@ -52,6 +56,10 @@ require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
@@ -65,9 +73,9 @@ require (
github.com/mitchellh/mapstructure v1.4.2 // indirect github.com/mitchellh/mapstructure v1.4.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mojocn/base64Captcha v1.3.6 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/spf13/afero v1.6.0 // indirect github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
@@ -79,8 +87,9 @@ require (
golang.org/x/crypto v0.33.0 // indirect golang.org/x/crypto v0.33.0 // indirect
golang.org/x/image v0.13.0 // indirect golang.org/x/image v0.13.0 // indirect
golang.org/x/net v0.34.0 // indirect golang.org/x/net v0.34.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/tools v0.26.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/ini.v1 v1.63.2 // indirect gopkg.in/ini.v1 v1.63.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect

View File

@@ -78,6 +78,7 @@ func (co *Config) AdminConfig(c *gin.Context) {
} }
hello := global.Config.Admin.Hello hello := global.Config.Admin.Hello
if hello == "" {
helloFile := global.Config.Admin.HelloFile helloFile := global.Config.Admin.HelloFile
if helloFile != "" { if helloFile != "" {
b, err := os.ReadFile(helloFile) b, err := os.ReadFile(helloFile)
@@ -85,6 +86,7 @@ func (co *Config) AdminConfig(c *gin.Context) {
hello = string(b) hello = string(b)
} }
} }
}
//replace {{username}} to username //replace {{username}} to username
hello = strings.Replace(hello, "{{username}}", u.Username, -1) hello = strings.Replace(hello, "{{username}}", u.Username, -1)

View File

@@ -38,7 +38,7 @@ func (f *File) Notify(c *gin.Context) {
res := global.Oss.Verify(c.Request) res := global.Oss.Verify(c.Request)
if !res { if !res {
response.Fail(c, 101, "权限错误") response.Fail(c, 101, response.TranslateMsg(c, "NoAccess"))
return return
} }
fm := &FileBack{} fm := &FileBack{}

View File

@@ -2,6 +2,7 @@ package admin
import ( import (
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global" "github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/controller/api" "github.com/lejianwen/rustdesk-api/v2/http/controller/api"
@@ -168,6 +169,8 @@ func (ct *Login) LoginOptions(c *gin.Context) {
"ops": ops, "ops": ops,
"register": global.Config.App.Register, "register": global.Config.App.Register,
"need_captcha": needCaptcha, "need_captcha": needCaptcha,
"disable_pwd": global.Config.App.DisablePwdLogin,
"auto_oidc": global.Config.App.DisablePwdLogin && len(ops) == 1,
}) })
} }
@@ -188,7 +191,7 @@ func (ct *Login) OidcAuth(c *gin.Context) {
return return
} }
err, state, verifier, nonce, url := service.AllService.OauthService.BeginAuth(f.Op) err, state, verifier, nonce, url := service.AllService.OauthService.BeginAuth(c, f.Op)
if err != nil { if err != nil {
response.Error(c, response.TranslateMsg(c, err.Error())) response.Error(c, response.TranslateMsg(c, err.Error()))
return return

View File

@@ -98,10 +98,10 @@ func (abc *AddressBookCollection) Update(c *gin.Context) {
return return
} }
u := service.AllService.UserService.CurUser(c) u := service.AllService.UserService.CurUser(c)
if f.UserId != u.Id { //if f.UserId != u.Id {
response.Fail(c, 101, response.TranslateMsg(c, "NoAccess")) // response.Fail(c, 101, response.TranslateMsg(c, "NoAccess"))
return // return
} //}
ex := service.AllService.AddressBookService.CollectionInfoById(f.Id) ex := service.AllService.AddressBookService.CollectionInfoById(f.Id)
if ex.Id == 0 { if ex.Id == 0 {
response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound")) response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound"))

View File

@@ -1,13 +1,14 @@
package admin package admin
import ( import (
"strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global" "github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin" "github.com/lejianwen/rustdesk-api/v2/http/request/admin"
adminReq "github.com/lejianwen/rustdesk-api/v2/http/request/admin" adminReq "github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response" "github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/service" "github.com/lejianwen/rustdesk-api/v2/service"
"strconv"
) )
type Oauth struct { type Oauth struct {
@@ -43,7 +44,7 @@ func (o *Oauth) ToBind(c *gin.Context) {
return return
} }
err, state, verifier, nonce, url := service.AllService.OauthService.BeginAuth(f.Op) err, state, verifier, nonce, url := service.AllService.OauthService.BeginAuth(c, f.Op)
if err != nil { if err != nil {
response.Error(c, response.TranslateMsg(c, err.Error())) response.Error(c, response.TranslateMsg(c, err.Error()))
return return
@@ -68,16 +69,16 @@ func (o *Oauth) Confirm(c *gin.Context) {
j := &adminReq.OauthConfirmForm{} j := &adminReq.OauthConfirmForm{}
err := c.ShouldBindJSON(j) err := c.ShouldBindJSON(j)
if err != nil { if err != nil {
response.Fail(c, 101, "参数错误"+err.Error()) response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
return return
} }
if j.Code == "" { if j.Code == "" {
response.Fail(c, 101, "参数错误: code 不存在") response.Fail(c, 101, response.TranslateMsg(c, "ParamsError"))
return return
} }
v := service.AllService.OauthService.GetOauthCache(j.Code) v := service.AllService.OauthService.GetOauthCache(j.Code)
if v == nil { if v == nil {
response.Fail(c, 101, "授权已过期") response.Fail(c, 101, response.TranslateMsg(c, "OauthExpired"))
return return
} }
u := service.AllService.UserService.CurUser(c) u := service.AllService.UserService.CurUser(c)

View File

@@ -8,6 +8,7 @@ import (
adResp "github.com/lejianwen/rustdesk-api/v2/http/response/admin" adResp "github.com/lejianwen/rustdesk-api/v2/http/response/admin"
"github.com/lejianwen/rustdesk-api/v2/model" "github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/service" "github.com/lejianwen/rustdesk-api/v2/service"
"github.com/lejianwen/rustdesk-api/v2/utils"
"gorm.io/gorm" "gorm.io/gorm"
"strconv" "strconv"
) )
@@ -243,11 +244,10 @@ func (ct *User) ChangeCurPwd(c *gin.Context) {
return return
} }
u := service.AllService.UserService.CurUser(c) u := service.AllService.UserService.CurUser(c)
// If the password is not empty, the old password is verified // Verify the old password only when the account already has one set
// otherwise, the old password is not verified
if !service.AllService.UserService.IsPasswordEmptyByUser(u) { if !service.AllService.UserService.IsPasswordEmptyByUser(u) {
oldPwd := service.AllService.UserService.EncryptPassword(f.OldPassword) ok, _, err := utils.VerifyPassword(u.Password, f.OldPassword)
if u.Password != oldPwd { if err != nil || !ok {
response.Fail(c, 101, response.TranslateMsg(c, "OldPasswordError")) response.Fail(c, 101, response.TranslateMsg(c, "OldPasswordError"))
return return
} }

View File

@@ -1,6 +1,8 @@
package api package api
import ( import (
"net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global" "github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/api" "github.com/lejianwen/rustdesk-api/v2/http/request/api"
@@ -10,7 +12,6 @@ import (
"github.com/lejianwen/rustdesk-api/v2/service" "github.com/lejianwen/rustdesk-api/v2/service"
"github.com/lejianwen/rustdesk-api/v2/utils" "github.com/lejianwen/rustdesk-api/v2/utils"
"github.com/nicksnyder/go-i18n/v2/i18n" "github.com/nicksnyder/go-i18n/v2/i18n"
"net/http"
) )
type Oauth struct { type Oauth struct {
@@ -35,7 +36,7 @@ func (o *Oauth) OidcAuth(c *gin.Context) {
oauthService := service.AllService.OauthService oauthService := service.AllService.OauthService
err, state, verifier, nonce, url := oauthService.BeginAuth(f.Op) err, state, verifier, nonce, url := oauthService.BeginAuth(c, f.Op)
if err != nil { if err != nil {
response.Error(c, response.TranslateMsg(c, err.Error())) response.Error(c, response.TranslateMsg(c, err.Error()))
return return
@@ -142,7 +143,7 @@ func (o *Oauth) OidcAuthQuery(c *gin.Context) {
// @Produce json // @Produce json
// @Success 200 {object} apiResp.LoginRes // @Success 200 {object} apiResp.LoginRes
// @Failure 500 {object} response.ErrorResponse // @Failure 500 {object} response.ErrorResponse
// @Router /oauth/callback [get] // @Router /oidc/callback [get]
func (o *Oauth) OauthCallback(c *gin.Context) { func (o *Oauth) OauthCallback(c *gin.Context) {
state := c.Query("state") state := c.Query("state")
if state == "" { if state == "" {
@@ -169,7 +170,7 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
var user *model.User var user *model.User
// 获取用户信息 // 获取用户信息
code := c.Query("code") code := c.Query("code")
err, oauthUser := oauthService.Callback(code, verifier, op, nonce) err, oauthUser := oauthService.Callback(c, code, verifier, op, nonce)
if err != nil { if err != nil {
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{ c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
"message": "OauthFailed", "message": "OauthFailed",
@@ -225,8 +226,7 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
if !*oauthConfig.AutoRegister { if !*oauthConfig.AutoRegister {
//c.String(http.StatusInternalServerError, "还未绑定用户,请先绑定") //c.String(http.StatusInternalServerError, "还未绑定用户,请先绑定")
oauthCache.UpdateFromOauthUser(oauthUser) oauthCache.UpdateFromOauthUser(oauthUser)
url := global.Config.Rustdesk.ApiServer + "/_admin/#/oauth/bind/" + cacheKey c.Redirect(http.StatusFound, "/_admin/#/oauth/bind/"+cacheKey)
c.Redirect(http.StatusFound, url)
return return
} }
@@ -251,8 +251,7 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
Type: model.LoginLogTypeOauth, Type: model.LoginLogTypeOauth,
Platform: oauthService.DeviceOs, Platform: oauthService.DeviceOs,
})*/ })*/
url := global.Config.Rustdesk.ApiServer + "/_admin/#/" c.Redirect(http.StatusFound, "/_admin/#/")
c.Redirect(http.StatusFound, url)
return return
} }
c.HTML(http.StatusOK, "oauth_success.html", gin.H{ c.HTML(http.StatusOK, "oauth_success.html", gin.H{

View File

@@ -1,6 +1,7 @@
package api package api
import ( import (
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
requstform "github.com/lejianwen/rustdesk-api/v2/http/request/api" requstform "github.com/lejianwen/rustdesk-api/v2/http/request/api"
@@ -13,7 +14,7 @@ type Peer struct {
} }
// SysInfo // SysInfo
// @Tags 地址 // @Tags System
// @Summary 提交系统信息 // @Summary 提交系统信息
// @Description 提交系统信息 // @Description 提交系统信息
// @Accept json // @Accept json
@@ -57,8 +58,19 @@ func (p *Peer) SysInfo(c *gin.Context) {
c.String(http.StatusOK, "SYSINFO_UPDATED") c.String(http.StatusOK, "SYSINFO_UPDATED")
} }
// SysInfoVer
// @Tags System
// @Summary 获取系统版本信息
// @Description 获取系统版本信息
// @Accept json
// @Produce json
// @Success 200 {string} string ""
// @Failure 500 {object} response.ErrorResponse
// @Router /sysinfo_ver [post]
func (p *Peer) SysInfoVer(c *gin.Context) { func (p *Peer) SysInfoVer(c *gin.Context) {
//读取resources/version文件 //读取resources/version文件
v := service.AllService.AppService.GetAppVersion() v := service.AllService.AppService.GetAppVersion()
// 加上启动时间方便client上传信息
v = fmt.Sprintf("%s\n%s", v, service.AllService.AppService.GetStartTime())
c.String(http.StatusOK, v) c.String(http.StatusOK, v)
} }

View File

@@ -13,13 +13,13 @@ func BackendUserAuth() gin.HandlerFunc {
//测试先关闭 //测试先关闭
token := c.GetHeader("api-token") token := c.GetHeader("api-token")
if token == "" { if token == "" {
response.Fail(c, 403, "请先登录") response.Fail(c, 403, response.TranslateMsg(c, "NeedLogin"))
c.Abort() c.Abort()
return return
} }
user, ut := service.AllService.UserService.InfoByAccessToken(token) user, ut := service.AllService.UserService.InfoByAccessToken(token)
if user.Id == 0 { if user.Id == 0 {
response.Fail(c, 403, "请先登录") response.Fail(c, 403, response.TranslateMsg(c, "NeedLogin"))
c.Abort() c.Abort()
return return
} }

View File

@@ -12,7 +12,7 @@ func AdminPrivilege() gin.HandlerFunc {
u := service.AllService.UserService.CurUser(c) u := service.AllService.UserService.CurUser(c)
if !service.AllService.UserService.IsAdmin(u) { if !service.AllService.UserService.IsAdmin(u) {
response.Fail(c, 403, "无权限") response.Fail(c, 403, response.TranslateMsg(c, "NoAccess"))
c.Abort() c.Abort()
return return
} }

View File

@@ -12,18 +12,18 @@ func JwtAuth() gin.HandlerFunc {
//测试先关闭 //测试先关闭
token := c.GetHeader("api-token") token := c.GetHeader("api-token")
if token == "" { if token == "" {
response.Fail(c, 403, "请先登录") response.Fail(c, 403, response.TranslateMsg(c, "NeedLogin"))
c.Abort() c.Abort()
return return
} }
uid, err := global.Jwt.ParseToken(token) uid, err := global.Jwt.ParseToken(token)
if err != nil { if err != nil {
response.Fail(c, 403, "请先登录") response.Fail(c, 403, response.TranslateMsg(c, "NeedLogin"))
c.Abort() c.Abort()
return return
} }
if uid == 0 { if uid == 0 {
response.Fail(c, 403, "请先登录") response.Fail(c, 403, response.TranslateMsg(c, "NeedLogin"))
c.Abort() c.Abort()
return return
} }
@@ -34,12 +34,12 @@ func JwtAuth() gin.HandlerFunc {
// Username: "测试用户", // Username: "测试用户",
//} //}
if user.Id == 0 { if user.Id == 0 {
response.Fail(c, 403, "请先登录") response.Fail(c, 403, response.TranslateMsg(c, "NeedLogin"))
c.Abort() c.Abort()
return return
} }
if !service.AllService.UserService.CheckUserEnable(user) { if !service.AllService.UserService.CheckUserEnable(user) {
response.Fail(c, 101, "你已被禁用") response.Fail(c, 101, response.TranslateMsg(c, "Banned"))
c.Abort() c.Abort()
return return
} }

View File

@@ -22,7 +22,6 @@ type OauthForm struct {
Scopes string `json:"scopes" validate:"omitempty"` Scopes string `json:"scopes" validate:"omitempty"`
ClientId string `json:"client_id" validate:"required"` ClientId string `json:"client_id" validate:"required"`
ClientSecret string `json:"client_secret" validate:"required"` ClientSecret string `json:"client_secret" validate:"required"`
RedirectUrl string `json:"redirect_url" validate:"required"`
AutoRegister *bool `json:"auto_register"` AutoRegister *bool `json:"auto_register"`
PkceEnable *bool `json:"pkce_enable"` PkceEnable *bool `json:"pkce_enable"`
PkceMethod string `json:"pkce_method"` PkceMethod string `json:"pkce_method"`
@@ -34,7 +33,6 @@ func (of *OauthForm) ToOauth() *model.Oauth {
OauthType: of.OauthType, OauthType: of.OauthType,
ClientId: of.ClientId, ClientId: of.ClientId,
ClientSecret: of.ClientSecret, ClientSecret: of.ClientSecret,
RedirectUrl: of.RedirectUrl,
AutoRegister: of.AutoRegister, AutoRegister: of.AutoRegister,
Issuer: of.Issuer, Issuer: of.Issuer,
Scopes: of.Scopes, Scopes: of.Scopes,

View File

@@ -14,6 +14,7 @@ type UserForm struct {
GroupId uint `json:"group_id" validate:"required"` GroupId uint `json:"group_id" validate:"required"`
IsAdmin *bool `json:"is_admin" ` IsAdmin *bool `json:"is_admin" `
Status model.StatusCode `json:"status" validate:"required,gte=0"` Status model.StatusCode `json:"status" validate:"required,gte=0"`
Remark string `json:"remark"`
} }
func (uf *UserForm) FromUser(user *model.User) *UserForm { func (uf *UserForm) FromUser(user *model.User) *UserForm {
@@ -25,6 +26,7 @@ func (uf *UserForm) FromUser(user *model.User) *UserForm {
uf.GroupId = user.GroupId uf.GroupId = user.GroupId
uf.IsAdmin = user.IsAdmin uf.IsAdmin = user.IsAdmin
uf.Status = user.Status uf.Status = user.Status
uf.Remark = user.Remark
return uf return uf
} }
func (uf *UserForm) ToUser() *model.User { func (uf *UserForm) ToUser() *model.User {
@@ -37,6 +39,7 @@ func (uf *UserForm) ToUser() *model.User {
user.GroupId = uf.GroupId user.GroupId = uf.GroupId
user.IsAdmin = uf.IsAdmin user.IsAdmin = uf.IsAdmin
user.Status = uf.Status user.Status = uf.Status
user.Remark = uf.Remark
return user return user
} }

View File

@@ -49,6 +49,10 @@ func ApiInit(g *gin.Engine) {
frg.GET("/oauth/callback", o.OauthCallback) frg.GET("/oauth/callback", o.OauthCallback)
frg.GET("/oauth/login", o.OauthCallback) frg.GET("/oauth/login", o.OauthCallback)
frg.GET("/oauth/msg", o.Message) frg.GET("/oauth/msg", o.Message)
frg.GET("/oidc/callback", o.OauthCallback)
frg.GET("/oidc/login", o.OauthCallback)
frg.GET("/oidc/msg", o.Message)
} }
{ {
pe := &api.Peer{} pe := &api.Peer{}

View File

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

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

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

View File

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

View File

@@ -14,6 +14,7 @@ const (
OauthTypeGoogle string = "google" OauthTypeGoogle string = "google"
OauthTypeOidc string = "oidc" OauthTypeOidc string = "oidc"
OauthTypeWebauth string = "webauth" OauthTypeWebauth string = "webauth"
OauthTypeLinuxdo string = "linuxdo"
PKCEMethodS256 string = "S256" PKCEMethodS256 string = "S256"
PKCEMethodPlain string = "plain" PKCEMethodPlain string = "plain"
) )
@@ -21,7 +22,7 @@ const (
// Validate the oauth type // Validate the oauth type
func ValidateOauthType(oauthType string) error { func ValidateOauthType(oauthType string) error {
switch oauthType { switch oauthType {
case OauthTypeGithub, OauthTypeGoogle, OauthTypeOidc, OauthTypeWebauth: case OauthTypeGithub, OauthTypeGoogle, OauthTypeOidc, OauthTypeWebauth, OauthTypeLinuxdo:
return nil return nil
default: default:
return errors.New("invalid Oauth type") return errors.New("invalid Oauth type")
@@ -30,6 +31,7 @@ func ValidateOauthType(oauthType string) error {
const ( const (
UserEndpointGithub string = "https://api.github.com/user" UserEndpointGithub string = "https://api.github.com/user"
UserEndpointLinuxdo string = "https://connect.linux.do/api/user"
IssuerGoogle string = "https://accounts.google.com" IssuerGoogle string = "https://accounts.google.com"
) )
@@ -39,7 +41,6 @@ type Oauth struct {
OauthType string `json:"oauth_type"` OauthType string `json:"oauth_type"`
ClientId string `json:"client_id"` ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret"` ClientSecret string `json:"client_secret"`
RedirectUrl string `json:"redirect_url"`
AutoRegister *bool `json:"auto_register"` AutoRegister *bool `json:"auto_register"`
Scopes string `json:"scopes"` Scopes string `json:"scopes"`
Issuer string `json:"issuer"` Issuer string `json:"issuer"`
@@ -60,6 +61,8 @@ func (oa *Oauth) FormatOauthInfo() error {
oa.Op = OauthTypeGithub oa.Op = OauthTypeGithub
case OauthTypeGoogle: case OauthTypeGoogle:
oa.Op = OauthTypeGoogle oa.Op = OauthTypeGoogle
case OauthTypeLinuxdo:
oa.Op = OauthTypeLinuxdo
} }
// check if the op is empty, set the default value // check if the op is empty, set the default value
op := strings.TrimSpace(oa.Op) op := strings.TrimSpace(oa.Op)
@@ -152,6 +155,24 @@ func (gu *GithubUser) ToOauthUser() *OauthUser {
} }
} }
type LinuxdoUser struct {
OauthUserBase
Id int `json:"id"`
Username string `json:"username"`
Avatar string `json:"avatar_url"`
}
func (lu *LinuxdoUser) ToOauthUser() *OauthUser {
return &OauthUser{
OpenId: strconv.Itoa(lu.Id),
Name: lu.Name,
Username: strings.ToLower(lu.Username),
Email: lu.Email,
VerifiedEmail: true, // linux.do 用户邮箱默认已验证
Picture: lu.Avatar,
}
}
type OauthList struct { type OauthList struct {
Oauths []*Oauth `json:"list"` Oauths []*Oauth `json:"list"`
Pagination Pagination

View File

@@ -11,6 +11,7 @@ type User struct {
GroupId uint `json:"group_id" gorm:"default:0;not null;index"` GroupId uint `json:"group_id" gorm:"default:0;not null;index"`
IsAdmin *bool `json:"is_admin" gorm:"default:0;not null;"` IsAdmin *bool `json:"is_admin" gorm:"default:0;not null;"`
Status StatusCode `json:"status" gorm:"default:1;not null;"` Status StatusCode `json:"status" gorm:"default:1;not null;"`
Remark string `json:"remark" gorm:"default:'';not null;"`
TimeModel TimeModel
} }

View File

@@ -33,6 +33,11 @@ description = "No access."
one = "No access." one = "No access."
other = "No access." other = "No access."
[NeedLogin]
description = "Need login."
one = "Please log in first."
other = "Please log in first."
[UsernameOrPasswordError] [UsernameOrPasswordError]
description = "Username or password error." description = "Username or password error."
one = "Username or password error." one = "Username or password error."

View File

@@ -33,6 +33,11 @@ description = "No access."
one = "Sin acceso." one = "Sin acceso."
other = "Sin acceso." other = "Sin acceso."
[NeedLogin]
description = "Need login."
one = "Por favor inicie sesión primero."
other = "Por favor inicie sesión primero."
[UsernameOrPasswordError] [UsernameOrPasswordError]
description = "Username or password error." description = "Username or password error."
one = "Error de usuario o contraseña." one = "Error de usuario o contraseña."

View File

@@ -33,6 +33,11 @@ description = "No access."
one = "Aucun d'access." one = "Aucun d'access."
other = "Aucun d'access." other = "Aucun d'access."
[NeedLogin]
description = "Need login."
one = "Veuillez d'abord vous connecter."
other = "Veuillez d'abord vous connecter."
[UsernameOrPasswordError] [UsernameOrPasswordError]
description = "Username or password error." description = "Username or password error."
one = "Nom d'utilisateur ou de mot de passe incorrect." one = "Nom d'utilisateur ou de mot de passe incorrect."

View File

@@ -33,6 +33,11 @@ description = "No access."
one = "접근할 수 없습니다." one = "접근할 수 없습니다."
other = "접근할 수 없습니다." other = "접근할 수 없습니다."
[NeedLogin]
description = "Need login."
one = "먼저 로그인해주세요."
other = "먼저 로그인해주세요."
[UsernameOrPasswordError] [UsernameOrPasswordError]
description = "Username or password error." description = "Username or password error."
one = "사용자 이름이나 비밀번호가 올바르지 않습니다." one = "사용자 이름이나 비밀번호가 올바르지 않습니다."

View File

@@ -33,6 +33,11 @@ description = "No access."
one = "Нет доступа." one = "Нет доступа."
other = "Нет доступа." other = "Нет доступа."
[NeedLogin]
description = "Need login."
one = "Пожалуйста, войдите в систему."
other = "Пожалуйста, войдите в систему."
[UsernameOrPasswordError] [UsernameOrPasswordError]
description = "Username or password error." description = "Username or password error."
one = "Неправильное имя пользователя или пароль." one = "Неправильное имя пользователя или пароль."

View File

@@ -33,6 +33,11 @@ description = "No access."
one = "无权限。" one = "无权限。"
other = "无权限。" other = "无权限。"
[NeedLogin]
description = "Need login."
one = "请先登录。"
other = "请先登录。"
[UsernameOrPasswordError] [UsernameOrPasswordError]
description = "Username or password error." description = "Username or password error."
one = "用户名或密码错误。" one = "用户名或密码错误。"

View File

@@ -5,8 +5,8 @@ other = "測試2 {{.P0}}"
[ParamsError] [ParamsError]
description = "Params validation failed." description = "Params validation failed."
one = "引數錯誤。" one = "參數驗證失敗。"
other = "引數錯誤。" other = "參數驗證失敗。"
[OperationFailed] [OperationFailed]
description = "OperationFailed." description = "OperationFailed."
@@ -20,18 +20,23 @@ other = "操作成功。"
[ItemExists] [ItemExists]
description = "Item already exists." description = "Item already exists."
one = "資料已存在。" one = "項目已存在。"
other = "資料已存在。" other = "項目已存在。"
[ItemNotFound] [ItemNotFound]
description = "Item not found." description = "Item not found."
one = "資料不存在。" one = "找不到項目。"
other = "資料不存在。" other = "找不到項目。"
[NoAccess] [NoAccess]
description = "No access." description = "No access."
one = "無許可權。" one = "無權限存取。"
other = "無許可權。" other = "無權限存取。"
[NeedLogin]
description = "Need login."
one = "請先登入。"
other = "請先登入。"
[UsernameOrPasswordError] [UsernameOrPasswordError]
description = "Username or password error." description = "Username or password error."
@@ -45,24 +50,23 @@ other = "系統錯誤。"
[ConfigNotFound] [ConfigNotFound]
description = "Config not found." description = "Config not found."
one = "配置不存在。" one = "找不到設定。"
other = "配置不存在。" other = "找不到設定。"
#授權過期
[OauthExpired] [OauthExpired]
description = "Oauth expired." description = "Oauth expired."
one = "授權過期,請重新授權。" one = "OAuth 已過期,請重。"
other = "授權過期,請重新授權。" other = "OAuth 已過期,請重。"
[OauthFailed] [OauthFailed]
description = "Oauth failed." description = "Oauth failed."
one = "授權失敗。" one = "OAuth 失敗。"
other = "授權失敗。" other = "OAuth 失敗。"
[OauthHasBindOtherUser] [OauthHasBindOtherUser]
description = "Oauth has bind other user." description = "Oauth has bind other user."
one = "授權已繫結其他使用者。" one = "OAuth 已綁定其他使用者。"
other = "授權已繫結其他使用者。" other = "OAuth 已綁定其他使用者。"
[ParamIsEmpty] [ParamIsEmpty]
description = "Param is empty." description = "Param is empty."
@@ -71,56 +75,64 @@ other = "{{.P0}} 為空。"
[BindFail] [BindFail]
description = "Bind fail." description = "Bind fail."
one = "繫結失敗。" one = "綁定失敗。"
other = "繫結失敗。" other = "綁定失敗。"
[BindSuccess] [BindSuccess]
description = "Bind success." description = "Bind success."
one = "繫結成功。" one = "綁定成功。"
other = "繫結成功。" other = "綁定成功。"
[OauthHasBeenSuccess] [OauthHasBeenSuccess]
description = "Oauth has been success." description = "Oauth has been success."
one = "授權已成功。" one = "OAuth 已成功。"
other = "授權已成功。" other = "OAuth 已成功。"
[OauthSuccess] [OauthSuccess]
description = "Oauth success." description = "Oauth success."
one = "授權成功。" one = "OAuth 成功。"
other = "授權成功。" other = "OAuth 成功。"
[OauthRegisterSuccess] [OauthRegisterSuccess]
description = "Oauth register success." description = "Oauth register success."
one = "授權註冊成功。" one = "OAuth 註冊成功。"
other = "授權註冊成功。" other = "OAuth 註冊成功。"
[OauthRegisterFailed] [OauthRegisterFailed]
description = "Oauth register failed." description = "Oauth register failed."
one = "授權註冊失敗。" one = "OAuth 註冊失敗。"
other = "授權註冊失敗。" other = "OAuth 註冊失敗。"
[GetOauthTokenError] [GetOauthTokenError]
description = "Get oauth token error." description = "Get oauth token error."
one = "獲取授權token失敗。" one = "取得 OAuth 權杖錯誤。"
other = "獲取授權token失敗。" other = "取得 OAuth 權杖錯誤。"
[GetOauthUserInfoError] [GetOauthUserInfoError]
description = "Get oauth user info error." description = "Get oauth user info error."
one = "獲取授權使用者資訊失敗。" one = "取得 OAuth 使用者資訊錯誤。"
other = "獲取授權使用者資訊失敗。" other = "取得 OAuth 使用者資訊錯誤。"
[DecodeOauthUserInfoError] [DecodeOauthUserInfoError]
description = "Decode oauth user info error." description = "Decode oauth user info error."
one = "解析授權使用者資訊失敗。" one = "解析 OAuth 使用者資訊錯誤。"
other = "解析授權使用者資訊失敗。" other = "解析 OAuth 使用者資訊錯誤。"
[OldPasswordError] [OldPasswordError]
description = "Old password error." description = "Old password error."
one = "舊密碼錯誤。" one = "舊密碼錯誤。"
other = "舊密碼錯誤。" other = "舊密碼錯誤。"
[DefaultGroup] [DefaultGroup]
description = "Default group." description = "Default group."
one = "預設組" one = "預設組"
other = "預設組" other = "預設組"
[ShareGroup] [ShareGroup]
description = "Share group." description = "Share group."
one = "共享組" one = "共享組"
other = "共享組" other = "共享組"
[RegisterClosed] [RegisterClosed]
description = "Register closed." description = "Register closed."
one = "註冊已關閉。" one = "註冊已關閉。"
@@ -138,20 +150,20 @@ other = "驗證碼錯誤。"
[PwdLoginDisabled] [PwdLoginDisabled]
description = "Password login disabled." description = "Password login disabled."
one = "密碼登錄已禁用。" one = "密碼登入已停用。"
other = "密碼登錄已禁用。" other = "密碼登入已停用。"
[CannotShareToSelf] [CannotShareToSelf]
description = "Cannot share to self." description = "Cannot share to self."
one = "無法享給自己。" one = "無法享給自己。"
other = "無法享給自己。" other = "無法享給自己。"
[Banned] [Banned]
description = "Banned." description = "Banned."
one = "禁止使用。" one = "已被禁用。"
other = "禁止使用。" other = "已被禁用。"
[RegisterSuccessWaitAdminConfirm] [RegisterSuccessWaitAdminConfirm]
description = "Register success wait admin confirm." description = "Register success, wait admin confirm."
one = "註冊成功,等待管理員確認。" one = "註冊成功,等待管理員確認。"
other = "註冊成功,等待管理員確認。" other = "註冊成功,等待管理員確認。"

View File

@@ -62,7 +62,7 @@
var title = 'OauthFailed' var title = 'OauthFailed'
var msg = '{{.message}}' var msg = '{{.message}}'
var btn = 'Close' var btn = 'Close'
document.writeln('<script src="/api/oauth/msg?lang=' + lang + '&msg=' + msg + '&title=OauthFailed"><\/script>'); document.writeln('<script src="/api/oidc/msg?lang=' + lang + '&msg=' + msg + '&title=OauthFailed"><\/script>');
</script> </script>
</head> </head>
<body> <body>

View File

@@ -61,7 +61,7 @@
var title = 'OauthSuccess' var title = 'OauthSuccess'
var msg = '{{.message}}' var msg = '{{.message}}'
var btn = 'Close' var btn = 'Close'
document.writeln('<script src="/api/oauth/msg?lang=' + lang + '&msg=' + msg + '&title=OauthSuccess"><\/script>'); document.writeln('<script src="/api/oidc/msg?lang=' + lang + '&msg=' + msg + '&title=OauthSuccess"><\/script>');
</script> </script>
</head> </head>
<body> <body>

View File

@@ -3,13 +3,14 @@ package service
import ( import (
"os" "os"
"sync" "sync"
"time"
) )
type AppService struct { type AppService struct {
} }
var version = "" var version = ""
var startTime = ""
var once = &sync.Once{} var once = &sync.Once{}
func (a *AppService) GetAppVersion() string { func (a *AppService) GetAppVersion() string {
@@ -26,3 +27,13 @@ func (a *AppService) GetAppVersion() string {
}) })
return version return version
} }
func init() {
// Initialize the AppService if needed
startTime = time.Now().Format("2006-01-02 15:04:05")
}
// GetStartTime
func (a *AppService) GetStartTime() string {
return startTime
}

View File

@@ -4,11 +4,14 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/model" "github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/utils" "github.com/lejianwen/rustdesk-api/v2/utils"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/oauth2/github" "golang.org/x/oauth2/github"
// "golang.org/x/oauth2/google" // "golang.org/x/oauth2/google"
"gorm.io/gorm" "gorm.io/gorm"
// "io" // "io"
@@ -93,16 +96,20 @@ func (os *OauthService) DeleteOauthCache(key string) {
OauthCache.Delete(key) OauthCache.Delete(key)
} }
func (os *OauthService) BeginAuth(op string) (error error, state, verifier, nonce, url string) { func (os *OauthService) BeginAuth(c *gin.Context, op string) (error error, state, verifier, nonce, url string) {
state = utils.RandomString(10) + strconv.FormatInt(time.Now().Unix(), 10) state = utils.RandomString(10) + strconv.FormatInt(time.Now().Unix(), 10)
verifier = "" verifier = ""
nonce = "" nonce = ""
if op == model.OauthTypeWebauth { if op == model.OauthTypeWebauth {
url = Config.Rustdesk.ApiServer + "/_admin/#/oauth/" + state host := c.GetHeader("Origin")
if host == "" {
host = Config.Rustdesk.ApiServer
}
url = host + "/_admin/#/oauth/" + state
//url = "http://localhost:8888/_admin/#/oauth/" + code //url = "http://localhost:8888/_admin/#/oauth/" + code
return nil, state, verifier, nonce, url return nil, state, verifier, nonce, url
} }
err, oauthInfo, oauthConfig, _ := os.GetOauthConfig(op) err, oauthInfo, oauthConfig, _ := os.GetOauthConfig(c, op)
if err == nil { if err == nil {
extras := make([]oauth2.AuthCodeOption, 0, 3) extras := make([]oauth2.AuthCodeOption, 0, 3)
@@ -154,21 +161,31 @@ func (os *OauthService) GithubProvider() *oidc.Provider {
}).NewProvider(context.Background()) }).NewProvider(context.Background())
} }
func (os *OauthService) LinuxdoProvider() *oidc.Provider {
return (&oidc.ProviderConfig{
IssuerURL: "",
AuthURL: "https://connect.linux.do/oauth2/authorize",
TokenURL: "https://connect.linux.do/oauth2/token",
DeviceAuthURL: "",
UserInfoURL: model.UserEndpointLinuxdo,
JWKSURL: "",
Algorithms: nil,
}).NewProvider(context.Background())
}
// GetOauthConfig retrieves the OAuth2 configuration based on the provider name // GetOauthConfig retrieves the OAuth2 configuration based on the provider name
func (os *OauthService) GetOauthConfig(op string) (err error, oauthInfo *model.Oauth, oauthConfig *oauth2.Config, provider *oidc.Provider) { func (os *OauthService) GetOauthConfig(c *gin.Context, op string) (err error, oauthInfo *model.Oauth, oauthConfig *oauth2.Config, provider *oidc.Provider) {
//err, oauthInfo, oauthConfig = os.getOauthConfigGeneral(op) //err, oauthInfo, oauthConfig = os.getOauthConfigGeneral(op)
oauthInfo = os.InfoByOp(op) oauthInfo = os.InfoByOp(op)
if oauthInfo.Id == 0 || oauthInfo.ClientId == "" || oauthInfo.ClientSecret == "" { if oauthInfo.Id == 0 || oauthInfo.ClientId == "" || oauthInfo.ClientSecret == "" {
return errors.New("ConfigNotFound"), nil, nil, nil return errors.New("ConfigNotFound"), nil, nil, nil
} }
// If the redirect URL is empty, use the default redirect URL redirectUrl := os.buildRedirectURL(c)
if oauthInfo.RedirectUrl == "" { Logger.Debug("Redirect URL: ", redirectUrl)
oauthInfo.RedirectUrl = Config.Rustdesk.ApiServer + "/api/oidc/callback"
}
oauthConfig = &oauth2.Config{ oauthConfig = &oauth2.Config{
ClientID: oauthInfo.ClientId, ClientID: oauthInfo.ClientId,
ClientSecret: oauthInfo.ClientSecret, ClientSecret: oauthInfo.ClientSecret,
RedirectURL: oauthInfo.RedirectUrl, RedirectURL: redirectUrl,
} }
// Maybe should validate the oauthConfig here // Maybe should validate the oauthConfig here
@@ -182,6 +199,10 @@ func (os *OauthService) GetOauthConfig(op string) (err error, oauthInfo *model.O
oauthConfig.Endpoint = github.Endpoint oauthConfig.Endpoint = github.Endpoint
oauthConfig.Scopes = []string{"read:user", "user:email"} oauthConfig.Scopes = []string{"read:user", "user:email"}
provider = os.GithubProvider() provider = os.GithubProvider()
case model.OauthTypeLinuxdo:
provider = os.LinuxdoProvider()
oauthConfig.Endpoint = provider.Endpoint()
oauthConfig.Scopes = []string{"profile"}
//case model.OauthTypeGoogle: //google单独出来可以少一次FetchOidcEndpoint请求 //case model.OauthTypeGoogle: //google单独出来可以少一次FetchOidcEndpoint请求
// oauthConfig.Endpoint = google.Endpoint // oauthConfig.Endpoint = google.Endpoint
// oauthConfig.Scopes = os.constructScopes(oauthInfo.Scopes) // oauthConfig.Scopes = os.constructScopes(oauthInfo.Scopes)
@@ -299,6 +320,16 @@ func (os *OauthService) githubCallback(oauthConfig *oauth2.Config, provider *oid
return nil, user.ToOauthUser() return nil, user.ToOauthUser()
} }
// linuxdoCallback linux.do回调
func (os *OauthService) linuxdoCallback(oauthConfig *oauth2.Config, provider *oidc.Provider, code, verifier, nonce string) (error, *model.OauthUser) {
var user = &model.LinuxdoUser{}
err, _ := os.callbackBase(oauthConfig, provider, code, verifier, nonce, user)
if err != nil {
return err, nil
}
return nil, user.ToOauthUser()
}
// oidcCallback oidc回调, 通过code获取用户信息 // oidcCallback oidc回调, 通过code获取用户信息
func (os *OauthService) oidcCallback(oauthConfig *oauth2.Config, provider *oidc.Provider, code, verifier, nonce 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{} var user = &model.OidcUser{}
@@ -309,8 +340,8 @@ func (os *OauthService) oidcCallback(oauthConfig *oauth2.Config, provider *oidc.
} }
// Callback: Get user information by code and op(Oauth provider) // Callback: Get user information by code and op(Oauth provider)
func (os *OauthService) Callback(code, verifier, op, nonce string) (err error, oauthUser *model.OauthUser) { func (os *OauthService) Callback(c *gin.Context, code, verifier, op, nonce string) (err error, oauthUser *model.OauthUser) {
err, oauthInfo, oauthConfig, provider := os.GetOauthConfig(op) err, oauthInfo, oauthConfig, provider := os.GetOauthConfig(c, op)
// oauthType is already validated in GetOauthConfig // oauthType is already validated in GetOauthConfig
if err != nil { if err != nil {
return err, nil return err, nil
@@ -319,6 +350,8 @@ func (os *OauthService) Callback(code, verifier, op, nonce string) (err error, o
switch oauthType { switch oauthType {
case model.OauthTypeGithub: case model.OauthTypeGithub:
err, oauthUser = os.githubCallback(oauthConfig, provider, code, verifier, nonce) err, oauthUser = os.githubCallback(oauthConfig, provider, code, verifier, nonce)
case model.OauthTypeLinuxdo:
err, oauthUser = os.linuxdoCallback(oauthConfig, provider, code, verifier, nonce)
case model.OauthTypeOidc, model.OauthTypeGoogle: case model.OauthTypeOidc, model.OauthTypeGoogle:
err, oauthUser = os.oidcCallback(oauthConfig, provider, code, verifier, nonce) err, oauthUser = os.oidcCallback(oauthConfig, provider, code, verifier, nonce)
default: default:
@@ -494,3 +527,22 @@ func (os *OauthService) getGithubPrimaryEmail(client *http.Client, githubUser *m
return fmt.Errorf("no primary verified email found") return fmt.Errorf("no primary verified email found")
} }
func (os *OauthService) buildRedirectURL(c *gin.Context) string {
baseUrl := Config.Rustdesk.ApiServer
host := c.Request.Host
if host != "" {
scheme := c.GetHeader("X-Forwarded-Proto")
if scheme == "" {
if c.Request.TLS != nil {
scheme = "https"
} else {
scheme = "http"
}
}
baseUrl = fmt.Sprintf("%s://%s", scheme, host)
}
return fmt.Sprintf("%s/api/oidc/callback", baseUrl)
}

View File

@@ -2,14 +2,14 @@ package service
import ( import (
"errors" "errors"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/utils"
"math/rand" "math/rand"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/utils"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -55,7 +55,18 @@ func (us *UserService) InfoByUsernamePassword(username, password string) *model.
Logger.Warn("Fallback to local database") Logger.Warn("Fallback to local database")
} }
u := &model.User{} u := &model.User{}
DB.Where("username = ? and password = ?", username, us.EncryptPassword(password)).First(u) DB.Where("username = ?", username).First(u)
if u.Id == 0 {
return u
}
ok, newHash, err := utils.VerifyPassword(u.Password, password)
if err != nil || !ok {
return &model.User{}
}
if newHash != "" {
DB.Model(u).Update("password", newHash)
u.Password = newHash
}
return u return u
} }
@@ -151,11 +162,6 @@ func (us *UserService) ListIdAndNameByGroupId(groupId uint) (res []*model.User)
return res return res
} }
// EncryptPassword 加密密码
func (us *UserService) EncryptPassword(password string) string {
return utils.Md5(password + "rustdesk-api")
}
// CheckUserEnable 判断用户是否禁用 // CheckUserEnable 判断用户是否禁用
func (us *UserService) CheckUserEnable(u *model.User) bool { func (us *UserService) CheckUserEnable(u *model.User) bool {
return u.Status == model.COMMON_STATUS_ENABLE return u.Status == model.COMMON_STATUS_ENABLE
@@ -168,7 +174,11 @@ func (us *UserService) Create(u *model.User) error {
return errors.New("UsernameExists") return errors.New("UsernameExists")
} }
u.Username = us.formatUsername(u.Username) u.Username = us.formatUsername(u.Username)
u.Password = us.EncryptPassword(u.Password) var err error
u.Password, err = utils.EncryptPassword(u.Password)
if err != nil {
return err
}
res := DB.Create(u).Error res := DB.Create(u).Error
return res return res
} }
@@ -268,8 +278,12 @@ func (us *UserService) FlushTokenByUuids(uuids []string) error {
// UpdatePassword 更新密码 // UpdatePassword 更新密码
func (us *UserService) UpdatePassword(u *model.User, password string) error { func (us *UserService) UpdatePassword(u *model.User, password string) error {
u.Password = us.EncryptPassword(password) var err error
err := DB.Model(u).Update("password", u.Password).Error u.Password, err = utils.EncryptPassword(password)
if err != nil {
return err
}
err = DB.Model(u).Update("password", u.Password).Error
if err != nil { if err != nil {
return err return err
} }
@@ -486,8 +500,9 @@ func (us *UserService) RefreshAccessToken(ut *model.UserToken) {
ut.ExpiredAt = us.UserTokenExpireTimestamp() ut.ExpiredAt = us.UserTokenExpireTimestamp()
DB.Model(ut).Update("expired_at", ut.ExpiredAt) DB.Model(ut).Update("expired_at", ut.ExpiredAt)
} }
func (us *UserService) AutoRefreshAccessToken(ut *model.UserToken) { func (us *UserService) AutoRefreshAccessToken(ut *model.UserToken) {
if ut.ExpiredAt-time.Now().Unix() < 86400 { if ut.ExpiredAt-time.Now().Unix() < Config.App.TokenExpire.Milliseconds()/3000 {
us.RefreshAccessToken(ut) us.RefreshAccessToken(ut)
} }
} }

View File

@@ -5,7 +5,8 @@ import (
"time" "time"
) )
var capdString = base64Captcha.NewDriverString(50, 150, 0, 5, 4, "123456789abcdefghijklmnopqrstuvwxyz", nil, nil, nil) var capdString = base64Captcha.NewDriverString(50, 150, 0, 5, 4, "123456789abcdefghijklmnopqrstuvwxyz", nil, nil,
[]string{"3Dumb.ttf", "ApothecaryFont.ttf", "Comismsh.ttf", "Flim-Flam.ttf", "RitaSmith.ttf", "wqy-microhei.ttc"})
var capdMath = base64Captcha.NewDriverMath(50, 150, 3, 10, nil, nil, nil) var capdMath = base64Captcha.NewDriverMath(50, 150, 3, 10, nil, nil, nil)

42
utils/password.go Normal file
View File

@@ -0,0 +1,42 @@
package utils
import (
"errors"
"golang.org/x/crypto/bcrypt"
)
// EncryptPassword hashes the input password using bcrypt.
// An error is returned if hashing fails.
func EncryptPassword(password string) (string, error) {
bs, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(bs), nil
}
// VerifyPassword checks the input password against the stored hash.
// When a legacy MD5 hash is provided, the password is rehashed with bcrypt
// and the new hash is returned. Any internal bcrypt error is returned.
func VerifyPassword(hash, input string) (bool, string, error) {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(input))
if err == nil {
return true, "", nil
}
var invalidPrefixErr bcrypt.InvalidHashPrefixError
if errors.As(err, &invalidPrefixErr) || errors.Is(err, bcrypt.ErrHashTooShort) {
// Try fallback to legacy MD5 hash verification
if hash == Md5(input+"rustdesk-api") {
newHash, err2 := bcrypt.GenerateFromPassword([]byte(input), bcrypt.DefaultCost)
if err2 != nil {
return true, "", err2
}
return true, string(newHash), nil
}
}
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return false, "", nil
}
return false, "", err
}

40
utils/password_test.go Normal file
View File

@@ -0,0 +1,40 @@
package utils
import (
"testing"
"golang.org/x/crypto/bcrypt"
)
func TestVerifyPasswordMD5(t *testing.T) {
hash := Md5("secret" + "rustdesk-api")
ok, newHash, err := VerifyPassword(hash, "secret")
if err != nil {
t.Fatalf("md5 verify failed: %v", err)
}
if !ok || newHash == "" {
t.Fatalf("md5 migration failed")
}
if bcrypt.CompareHashAndPassword([]byte(newHash), []byte("secret")) != nil {
t.Fatalf("invalid rehash")
}
}
func TestVerifyPasswordBcrypt(t *testing.T) {
b, _ := bcrypt.GenerateFromPassword([]byte("pass"), bcrypt.DefaultCost)
ok, newHash, err := VerifyPassword(string(b), "pass")
if err != nil || !ok || newHash != "" {
t.Fatalf("bcrypt verify failed")
}
}
func TestVerifyPasswordMigrate(t *testing.T) {
md5hash := Md5("mypass" + "rustdesk-api")
ok, newHash, err := VerifyPassword(md5hash, "mypass")
if err != nil || !ok || newHash == "" {
t.Fatalf("expected bcrypt rehash")
}
if bcrypt.CompareHashAndPassword([]byte(newHash), []byte("mypass")) != nil {
t.Fatalf("rehash not valid bcrypt")
}
}

View File

@@ -2,9 +2,9 @@ package utils
import ( import (
"crypto/md5" "crypto/md5"
crand "crypto/rand"
"encoding/json" "encoding/json"
"fmt" "fmt"
"math/rand"
"reflect" "reflect"
"runtime/debug" "runtime/debug"
"strings" "strings"
@@ -69,8 +69,12 @@ func RandomString(n int) string {
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
length := len(letterBytes) length := len(letterBytes)
b := make([]byte, n) b := make([]byte, n)
for i := range b { randomBytes := make([]byte, n)
b[i] = letterBytes[rand.Intn(length)] if _, err := crand.Read(randomBytes); err != nil {
return ""
}
for i, rb := range randomBytes {
b[i] = letterBytes[int(rb)%length]
} }
return string(b) return string(b)
} }