Compare commits

...

64 Commits

Author SHA1 Message Date
lejianwen
dbf8b23b15 fix: Config watchConfig (#135)
---
Closes: #135
2025-02-10 10:13:34 +08:00
lejianwen
79a5dd53ae fix: User disabled can not work (#133)
---
Closes: #133
2025-02-10 10:13:15 +08:00
Tao Chen
8a5b20685c fix: When OIDC and LDAP work togethar (#132 #134)
* fix OIDC create user if LDAP enable

* `newUser.GroupId = 1` for ldap

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

* update

* cleanup

* tmp save group mapping

* add enableControl(not-test)

* verify username exist before create(for LDAP)

* add getAllGroupsDn()

* rename

* adminGroup

* enable TLS Verify

* init for ldap

---------

Co-authored-by: Tao Chen <iamtaochen@outlook.com>
2025-02-02 23:59:52 +08:00
lejianwen
f49457dc5b feat(webclient): Up to 1.3.7 2025-01-21 19:12:28 +08:00
lejianwen
d9e2e247ea feat(api): Add api token expire
Resolves #109
2025-01-21 18:23:28 +08:00
lejianwen
c6f2f2f150 feat(api): Add api/version
Resolves #110
2025-01-20 20:04:22 +08:00
lejianwen
56b9c66cb8 docs: Up Swagger docs 2025-01-20 19:35:47 +08:00
lejianwen
5d8a0d0e1f style: Add Start Tips 2025-01-20 13:12:45 +08:00
lejianwen
f4cb9beda5 fix(api): Change tag to alphabetical sorting
Fixes: #108
2025-01-20 13:07:41 +08:00
lejianwen
b66fc3c06d fix(docs): Api Route doc 2025-01-19 13:10:43 +08:00
lejianwen
ab2e1a9236 feat(i18n): Add ZH_TW 2025-01-19 13:10:19 +08:00
Jia-Bin
ab77b400a1 Add Traditional Chinese 2025-01-18 23:04:39 +08:00
lejianwen
eb7ab63563 docs: Up readme 2025-01-16 22:01:23 +08:00
lejianwen
4cf7d01622 docs: Up readme 2025-01-16 21:59:46 +08:00
lejianwen
a876078a9c feat(server): Rustdesk Id Server Port & Relay Server Port #104 2025-01-16 20:57:00 +08:00
lejianwen
495f2ae3c6 refactor(config): Up Config Load 2025-01-16 20:40:42 +08:00
lejianwen
4e6d11baf0 docs: Up readme 2025-01-15 21:56:04 +08:00
lejianwen
a951b982b3 fix: Jwt 2025-01-15 20:26:26 +08:00
lejianwen
a33be66504 docs: Up readme 2025-01-15 20:09:08 +08:00
lejianwen
f41b9d5887 feat!: Add JWT
- `RUSTDESK_API_JWT_KEY`如果设置,将会启用JWT,token自动续期功能将失效
- 此功能是为了server端校验token的合法性
2025-01-15 19:25:28 +08:00
lejianwen
3c608463e6 docs: Up readme 2025-01-12 23:12:00 +08:00
lejianwen
eeffbe124a docs: Up readme 2025-01-12 21:35:34 +08:00
lejianwen
d7f2d54faa feat(server): Add Rustdesk Relay Server Commands 2025-01-04 20:49:44 +08:00
lejianwen
7db4b03634 style(server): fmt print to log 2025-01-02 21:49:37 +08:00
lejianwen
77760a681a docs: Up readme 2025-01-02 17:03:07 +08:00
lejianwen
f9c1447ceb fix: Fix Dockerfile_full_s6 2024-12-31 23:33:17 +08:00
lejianwen
fb749c1902 fix(server): Fix Rustdesk Sys Command 2024-12-31 23:29:05 +08:00
lejianwen
240c44aa07 feat(server): Add Rustdesk Command
And add build full s6 image for rustdesk command
2024-12-31 23:16:15 +08:00
lejianwen
92cd8642c8 docs(readme): Up readme
Connection timeout issues move to #92
2024-12-30 13:36:57 +08:00
lejianwen
89d90cf919 docs(readme): Up readme
Connection timeout issues move to #92
2024-12-30 13:35:06 +08:00
lejianwen
920c6b6d8b docs(readme): Up readme
Connection timeout issues move to #92
2024-12-30 13:33:11 +08:00
lejianwen
1dd4df3a1c chore(buildTest): add start.bat to run on windows 2024-12-27 20:13:55 +08:00
lejianwen
c7d44cc253 fix(build): add start.bat to run on windows(#89) 2024-12-27 20:09:08 +08:00
lejianwen
5082ab1893 refactor(admin): Move Admin Web Route to user model 2024-12-27 19:27:33 +08:00
lejianwen
e8b2425222 feat(admin): Add My Login log 2024-12-27 19:25:59 +08:00
lejianwen
09d12cefd8 feat(admin): Support Markdown to welcome msg 2024-12-25 19:21:50 +08:00
lejianwen
5f1166965d fix(api): Get ab list when personal is disabled (#86) 2024-12-25 19:04:42 +08:00
lejianwen
0dbab182e9 fix(webclient): share fail when expire is 0
Closes: #88
2024-12-25 15:01:03 +08:00
lejianwen
512f3f99fd fix(build): fix build_test.yml 2024-12-25 14:06:29 +08:00
lejianwen
6fb4fad705 fix(build): up build.yml to build deb 2024-12-25 13:20:12 +08:00
lejianwen
fa92529e9b chore(build): up build.yml to build deb 2024-12-25 12:57:11 +08:00
Follow the wind
d6c6051a6c feat(build): 添加构建deb包相关基础 (#87)
* 添加构建deb包相关基础

* 补齐工作流,等待验证

* 修复构建时没有创建的data目录保障deb包构建

* 修复其余架构deb包构建中的依赖错误

* 修复:由于小改工作流导致写错架构的问题

* 修复拼写错误导致的目录错误

* 添加上传工件,和发布rel工作流,完成相关事务

---------

Co-authored-by: ymwl <ymwlpoolc@qq.com>
2024-12-25 12:28:51 +08:00
lejianwen
ce063bd3ac feat(webclient): v1.3.5 -> v1.3.6 2024-12-24 10:25:47 +08:00
lejianwen
4468894dfb chore(changelog): up build.yml to generate changelog 2024-12-22 14:14:51 +08:00
lejianwen
8b00b919ad chore(changelog): up build.yml to generate changelog 2024-12-22 14:04:23 +08:00
64f4a6dfac feat(i18n): Merge pull request #85 from jimmyGALLAND/trans-fr
add locale french
2024-12-22 13:57:56 +08:00
jimmyGALLAND
6faa5153b6 add locale french 2024-12-21 22:56:48 +01:00
lejianwen
a771b1e9b0 fix(webclient): remove console.log when query online by new 2024-12-21 21:49:03 +08:00
lejianwen
7750f9c54d chore(changelog): up build.yml to add changelog 2024-12-21 21:39:11 +08:00
lejianwen
b2d24ee67b docs(webclient): up readme 2024-12-21 21:31:54 +08:00
lejianwen
589a2a5123 feat(webclient): add new query_online function
There may be a loss of performance
Therefore, it is not enabled by default
2024-12-21 21:15:06 +08:00
75 changed files with 51193 additions and 48671 deletions

View File

@@ -55,6 +55,13 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/checkout@v4
with:
repository: lejianwen/rustdesk-api-web
path: rustdesk-api-web
ref: master
- name: Set up Go environment - name: Set up Go environment
uses: actions/setup-go@v4 uses: actions/setup-go@v4
@@ -66,19 +73,26 @@ jobs:
with: with:
node-version: '20' node-version: '20'
- name: build rustdesk-api-web - name: build rustdesk-api-web
working-directory: rustdesk-api-web
run: | run: |
git clone ${{ env.WEBCLIENT_SOURCE_LOCATION }}
cd rustdesk-api-web
npm install npm install
npm run build npm run build
mkdir ../resources/admin/ -p mkdir -p ../resources/admin/
cp -ar dist/* ../resources/admin/ cp -ar dist/* ../resources/admin/
- name: tidy - name: tidy
run: go mod tidy run: go mod tidy
- name: Get tag version
run: |
TAG_VERSION="${GITHUB_REF##*/}"
VERSION="${TAG_VERSION#v}"
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Write version to resources/version
run: echo $VERSION > resources/version
- name: swag - name: swag
run: | run: |
go install github.com/swaggo/swag/cmd/swag@latest go install github.com/swaggo/swag/cmd/swag@latest
@@ -96,6 +110,8 @@ jobs:
if [ "${{ matrix.job.goos }}" = "windows" ]; then if [ "${{ matrix.job.goos }}" = "windows" ]; then
sudo apt-get install gcc-mingw-w64-x86-64 zip -y sudo apt-get install gcc-mingw-w64-x86-64 zip -y
GOOS=${{ matrix.job.goos }} GOARCH=${{ matrix.job.platform }} CC=x86_64-w64-mingw32-gcc CGO_LDFLAGS="-static" CGO_ENABLED=1 go build -ldflags "-s -w" -o ./release/apimain.exe ./cmd/apimain.go GOOS=${{ matrix.job.goos }} GOARCH=${{ matrix.job.platform }} CC=x86_64-w64-mingw32-gcc CGO_LDFLAGS="-static" CGO_ENABLED=1 go build -ldflags "-s -w" -o ./release/apimain.exe ./cmd/apimain.go
echo @echo off > release/start.bat
echo cmd /c \"%~dp0apimain.exe\" >> release/start.bat
zip -r ${{ matrix.job.goos}}-${{ matrix.job.platform }}.${{matrix.job.file_ext}} ./release zip -r ${{ matrix.job.goos}}-${{ matrix.job.platform }}.${{matrix.job.file_ext}} ./release
else else
if [ "${{ matrix.job.platform }}" = "arm64" ]; then if [ "${{ matrix.job.platform }}" = "arm64" ]; then
@@ -121,15 +137,93 @@ jobs:
name: rustdesk-api-${{ matrix.job.goos }}-${{ matrix.job.platform }} name: rustdesk-api-${{ matrix.job.goos }}-${{ matrix.job.platform }}
path: | path: |
${{ matrix.job.goos}}-${{ matrix.job.platform }}.${{matrix.job.file_ext}} ${{ matrix.job.goos}}-${{ matrix.job.platform }}.${{matrix.job.file_ext}}
- name: Upload to GitHub Release - name: Upload to GitHub Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: | files: |
${{ matrix.job.goos}}-${{ matrix.job.platform }}.${{matrix.job.file_ext}} ${{ matrix.job.goos}}-${{ matrix.job.platform }}.${{matrix.job.file_ext}}
# tag_name: ${{ env.LATEST_TAG }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate Changelog
run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
deb-package:
name: debian package - ${{ matrix.job.platform }}
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
job:
- { platform: "amd64", goos: "linux", debian_platform: "amd64", crossbuild_package: ""}
- { platform: "arm64", goos: "linux", debian_platform: "arm64", crossbuild_package: "crossbuild-essential-arm64" }
- { platform: "armv7l", goos: "linux", debian_platform: "armhf", crossbuild_package: "crossbuild-essential-armhf" }
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Create packaging env
run: |
sudo apt update
DEBIAN_FRONTEND=noninteractive sudo apt install -y devscripts build-essential debhelper pkg-config ${{ matrix.job.crossbuild_package }}
mkdir -p debian-build/${{ matrix.job.platform }}/bin
- name: Get tag version
id: get_tag
run: |
TAG_VERSION="${GITHUB_REF##*/}"
VERSION="${TAG_VERSION#v}"
echo "TAG_VERSION=$TAG_VERSION" >> $GITHUB_ENV
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Update changelog
run: |
DATE=$(date -R)
sed -i "1i rustdesk-api-server (${VERSION}) stable; urgency=medium\n\n * Automatically generated release for version ${VERSION}.\n\n -- GitHub Actions <actions@github.com> ${DATE}\n" debian/changelog
- name: Download binaries
uses: actions/download-artifact@v4
with:
name: rustdesk-api-${{ matrix.job.goos }}-${{ matrix.job.platform }}
path: .
- name: Unzip binaries
run: |
mkdir -p ${{ matrix.job.platform }}
tar -xzf ${{ matrix.job.goos }}-${{ matrix.job.platform }}.tar.gz -C ${{ matrix.job.platform }}
- name: Build package for ${{ matrix.job.platform }} arch
run: |
mv ${{ matrix.job.platform }}/release/apimain debian-build/${{ matrix.job.platform }}/bin/rustdesk-api
mv ${{ matrix.job.platform }}/release/resources/admin resources
chmod -v a+x debian-build/${{ matrix.job.platform }}/bin/*
mkdir -p data
cp -vr debian systemd conf data resources runtime debian-build/${{ matrix.job.platform }}/
cat debian/control.tpl | sed 's/{{ ARCH }}/${{ matrix.job.debian_platform }}/' > debian-build/${{ matrix.job.platform }}/debian/control
cd debian-build/${{ matrix.job.platform }}/
debuild -i -us -uc -b -a${{ matrix.job.debian_platform}}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: rustdesk-api-${{ matrix.job.debian_platform }}
path: |
debian-build/*.deb
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
debian-build/rustdesk-api-server_*_${{ matrix.job.debian_platform }}.deb
docker: docker:
name: Push Docker Image name: Push Docker Image
needs: build needs: build
@@ -191,7 +285,6 @@ jobs:
run: | run: |
mkdir -p ${{ matrix.job.platform }} mkdir -p ${{ matrix.job.platform }}
tar -xzf ${{ matrix.job.goos }}-${{ matrix.job.platform }}.tar.gz -C ${{ matrix.job.platform }} tar -xzf ${{ matrix.job.goos }}-${{ matrix.job.platform }}.tar.gz -C ${{ matrix.job.platform }}
file ${{ matrix.job.platform }}/apimain
- name: Build and push Docker image to Docker Hub ${{ matrix.job.platform }} - name: Build and push Docker image to Docker Hub ${{ matrix.job.platform }}
if: ${{ env.SKIP_DOCKER_HUB == 'false' }} # Only run this step if SKIP_DOCKER_HUB is false if: ${{ env.SKIP_DOCKER_HUB == 'false' }} # Only run this step if SKIP_DOCKER_HUB is false
@@ -209,6 +302,21 @@ jobs:
${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-${{ matrix.job.platform }} ${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-${{ matrix.job.platform }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
- name: Build and push Docker Full S6 image to Docker Hub ${{ matrix.job.platform }}
if: ${{ env.SKIP_DOCKER_HUB == 'false' }} # Only run this step if SKIP_DOCKER_HUB is false
uses: docker/build-push-action@v5
with:
context: "."
file: ./Dockerfile_full_s6
platforms: ${{ matrix.job.docker_platform }}
push: true
provenance: false
build-args: |
BUILDARCH=${{ matrix.job.platform }}
tags: |
${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:full-s6-${{ matrix.job.platform }}
labels: ${{ steps.meta.outputs.labels }}
- name: Build and push Docker image to GHCR ${{ matrix.job.platform }} - name: Build and push Docker image to GHCR ${{ matrix.job.platform }}
if: ${{ env.SKIP_GHCR == 'false' }} # Only run this step if SKIP_GHCR is false if: ${{ env.SKIP_GHCR == 'false' }} # Only run this step if SKIP_GHCR is false
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
@@ -225,6 +333,21 @@ jobs:
ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-${{ matrix.job.platform }} ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-${{ matrix.job.platform }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
- name: Build and push Docker Full S6 image to GHCR ${{ matrix.job.platform }}
if: ${{ env.SKIP_GHCR == 'false' }} # Only run this step if SKIP_GHCR is false
uses: docker/build-push-action@v5
with:
context: "."
file: ./Dockerfile
platforms: ${{ matrix.job.docker_platform }}
push: true
provenance: false
build-args: |
BUILDARCH=${{ matrix.job.platform }}
tags: |
ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:full-s6-${{ matrix.job.platform }}
labels: ${{ steps.meta.outputs.labels }}
# #
docker-manifest: docker-manifest:
name: Push Docker Manifest name: Push Docker Manifest
@@ -295,4 +418,26 @@ jobs:
ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:latest-armv7l, ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:latest-armv7l,
ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:latest-arm64 ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:latest-arm64
push: true push: true
amend: true
- name: Create and push Full S6 manifest Docker Hub (:version)
if: ${{ env.SKIP_DOCKER_HUB == 'false' }}
uses: Noelware/docker-manifest-action@master
with:
base-image: ${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:full-s6
extra-images: ${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:full-s6-amd64,
${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:full-s6-armv7l,
${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:full-s6-arm64
push: true
amend: true
- name: Create and push Full S6 manifest GHCR (:latest)
if: ${{ env.SKIP_GHCR == 'false' }}
uses: Noelware/docker-manifest-action@master
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,
ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:full-s6-armv7l,
ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:full-s6-arm64
push: true
amend: true amend: true

View File

@@ -52,6 +52,12 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: lejianwen/rustdesk-api-web
path: rustdesk-api-web
ref: master
- name: Set up Go environment - name: Set up Go environment
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
@@ -62,14 +68,12 @@ jobs:
with: with:
node-version: '20' node-version: '20'
- name: build rustdesk-api-web - name: build rustdesk-api-web
working-directory: rustdesk-api-web
run: | run: |
git clone ${{ env.WEBCLIENT_SOURCE_LOCATION }}
cd rustdesk-api-web
npm install npm install
npm run build npm run build
mkdir ../resources/admin/ -p mkdir -p ../resources/admin/
cp -ar dist/* ../resources/admin/ cp -ar dist/* ../resources/admin/
- name: tidy - name: tidy
@@ -92,6 +96,8 @@ jobs:
if [ "${{ matrix.job.goos }}" = "windows" ]; then if [ "${{ matrix.job.goos }}" = "windows" ]; then
sudo apt-get install gcc-mingw-w64-x86-64 zip -y sudo apt-get install gcc-mingw-w64-x86-64 zip -y
GOOS=${{ matrix.job.goos }} GOARCH=${{ matrix.job.platform }} CC=x86_64-w64-mingw32-gcc CGO_LDFLAGS="-static" CGO_ENABLED=1 go build -ldflags "-s -w" -o ./release/apimain.exe ./cmd/apimain.go GOOS=${{ matrix.job.goos }} GOARCH=${{ matrix.job.platform }} CC=x86_64-w64-mingw32-gcc CGO_LDFLAGS="-static" CGO_ENABLED=1 go build -ldflags "-s -w" -o ./release/apimain.exe ./cmd/apimain.go
echo @echo off > release/start.bat
echo cmd /c \"%~dp0apimain.exe\" >> release/start.bat
zip -r ${{ matrix.job.goos}}-${{ matrix.job.platform }}.${{matrix.job.file_ext}} ./release zip -r ${{ matrix.job.goos}}-${{ matrix.job.platform }}.${{matrix.job.file_ext}} ./release
else else
if [ "${{ matrix.job.platform }}" = "arm64" ]; then if [ "${{ matrix.job.platform }}" = "arm64" ]; then
@@ -117,6 +123,7 @@ jobs:
name: rustdesk-api-${{ matrix.job.goos }}-${{ matrix.job.platform }} name: rustdesk-api-${{ matrix.job.goos }}-${{ matrix.job.platform }}
path: | path: |
${{ matrix.job.goos}}-${{ matrix.job.platform }}.${{matrix.job.file_ext}} ${{ matrix.job.goos}}-${{ matrix.job.platform }}.${{matrix.job.file_ext}}
- name: Upload to GitHub Release - name: Upload to GitHub Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
@@ -126,6 +133,66 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
deb-package:
name: debian package - ${{ matrix.job.platform }}
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
job:
- { platform: "amd64", goos: "linux", debian_platform: "amd64", crossbuild_package: ""}
- { platform: "arm64", goos: "linux", debian_platform: "arm64", crossbuild_package: "crossbuild-essential-arm64" }
- { platform: "armv7l", goos: "linux", debian_platform: "armhf", crossbuild_package: "crossbuild-essential-armhf" }
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Create packaging env
run: |
sudo apt update
DEBIAN_FRONTEND=noninteractive sudo apt install -y devscripts build-essential debhelper pkg-config ${{ matrix.job.crossbuild_package }}
mkdir -p debian-build/${{ matrix.job.platform }}/bin
- name: Download binaries
uses: actions/download-artifact@v4
with:
name: rustdesk-api-${{ matrix.job.goos }}-${{ matrix.job.platform }}
path: .
- name: Unzip binaries
run: |
mkdir -p ${{ matrix.job.platform }}
tar -xzf ${{ matrix.job.goos }}-${{ matrix.job.platform }}.tar.gz -C ${{ matrix.job.platform }}
- name: Build package for ${{ matrix.job.platform }} arch
run: |
mv ${{ matrix.job.platform }}/release/apimain debian-build/${{ matrix.job.platform }}/bin/rustdesk-api
chmod -v a+x debian-build/${{ matrix.job.platform }}/bin/*
mkdir -p data
cp -vr debian systemd conf data resources runtime debian-build/${{ matrix.job.platform }}/
cat debian/control.tpl | sed 's/{{ ARCH }}/${{ matrix.job.debian_platform }}/' > debian-build/${{ matrix.job.platform }}/debian/control
cd debian-build/${{ matrix.job.platform }}/
debuild -i -us -uc -b -a${{ matrix.job.debian_platform}}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: rustdesk-api-${{ matrix.job.debian_platform }}
path: |
debian-build/*.deb
- name: Upload to GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: test
files: |
debian-build/rustdesk-api-server_*_${{ matrix.job.debian_platform }}.deb
docker: docker:
name: Push Docker Image name: Push Docker Image
needs: build needs: build
@@ -187,7 +254,6 @@ jobs:
run: | run: |
mkdir -p ${{ matrix.job.platform }} mkdir -p ${{ matrix.job.platform }}
tar -xzf ${{ matrix.job.goos }}-${{ matrix.job.platform }}.tar.gz -C ${{ matrix.job.platform }} tar -xzf ${{ matrix.job.goos }}-${{ matrix.job.platform }}.tar.gz -C ${{ matrix.job.platform }}
file ${{ matrix.job.platform }}/apimain
- name: Build and push Docker image to Docker Hub ${{ matrix.job.platform }} - name: Build and push Docker image to Docker Hub ${{ matrix.job.platform }}
if: ${{ env.SKIP_DOCKER_HUB == 'false' }} # Only run this step if SKIP_DOCKER_HUB is false if: ${{ env.SKIP_DOCKER_HUB == 'false' }} # Only run this step if SKIP_DOCKER_HUB is false

7
.gitignore vendored
View File

@@ -1,13 +1,8 @@
.idea .idea
runtime/* runtime/*
!runtime !runtime
!runtime/cache
!runtime/cache/.gitkeep !runtime/cache/.gitkeep
go.sum go.sum
resources/* resources/admin
!resources/public/upload/.gitignore
!resources/web
!resources/web2
!resources/i18n
release release
data data

View File

@@ -2,9 +2,8 @@ FROM alpine
ARG BUILDARCH ARG BUILDARCH
WORKDIR /app WORKDIR /app
RUN apk add --no-cache tzdata file RUN apk add --no-cache tzdata
COPY ./${BUILDARCH}/release /app/ COPY ./${BUILDARCH}/release /app/
RUN file /app/apimain
VOLUME /app/data VOLUME /app/data
EXPOSE 21114 EXPOSE 21114

38
Dockerfile_full_s6 Normal file
View File

@@ -0,0 +1,38 @@
FROM rustdesk/rustdesk-server-s6:latest AS server
FROM alpine
ARG BUILDARCH
WORKDIR /app
RUN apk add --no-cache tzdata
COPY ./${BUILDARCH}/release /app/
COPY --from=server /init /init
COPY --from=server /etc/s6-overlay /etc/s6-overlay
COPY --from=server /package /package
COPY --from=server /usr/bin/healthcheck.sh /usr/bin/healthcheck.sh
COPY --from=server /usr/bin/hbbr /usr/bin/hbbr
COPY --from=server /usr/bin/hbbs /usr/bin/hbbs
COPY --from=server /usr/bin/rustdesk-utils /usr/bin/rustdesk-utils
COPY --from=server /command /command
RUN \
mkdir -p /etc/s6-overlay/s6-rc.d/api && \
echo -e "key-secret\nhbbs" > /etc/s6-overlay/s6-rc.d/api/dependencies && \
echo "longrun" > /etc/s6-overlay/s6-rc.d/api/type && \
echo "#!/command/with-contenv sh" > /etc/s6-overlay/s6-rc.d/api/run && \
echo "cd /app" >> /etc/s6-overlay/s6-rc.d/api/run && \
echo "./apimain" >> /etc/s6-overlay/s6-rc.d/api/run && \
touch /etc/s6-overlay/s6-rc.d/user/contents.d/api && \
echo "/package/admin/s6/command/s6-svstat /run/s6-rc/servicedirs/api || exit 1" >> /usr/bin/healthcheck.sh && \
ln -s /run /var/run
ENV RELAY=relay.example.com
ENV ENCRYPTED_ONLY=0
VOLUME /data
VOLUME /app/data
EXPOSE 21114 21115 21116 21116/udp 21117 21118 21119
ENTRYPOINT ["/init"]

272
README.md
View File

@@ -9,6 +9,7 @@
<img src="https://img.shields.io/badge/gin-v1.9.0-lightBlue"/> <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/gorm-v1.25.7-green"/>
<img src="https://img.shields.io/badge/swag-v1.16.3-yellow"/> <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://github.com/lejianwen/rustdesk-api/actions/workflows/build.yml/badge.svg"/> <img src="https://github.com/lejianwen/rustdesk-api/actions/workflows/build.yml/badge.svg"/>
</div> </div>
@@ -19,7 +20,10 @@
- 登录 - 登录
- 地址簿 - 地址簿
- 群组 - 群组
- 授权登录,支持`github`, `google``OIDC` 登录,支持`web后台`授权登录 - 授权登录
- 支持`github`, `google``OIDC` 登录,
- 支持`web后台`授权登录
- 支持`LDAP`(AD和OpenLDAP已测试), 如果API Server配置了LDAP
- i18n - i18n
- Web Admin - Web Admin
- 用户管理 - 用户管理
@@ -28,12 +32,14 @@
- 标签管理 - 标签管理
- 群组管理 - 群组管理
- Oauth 管理 - Oauth 管理
- 配置LDAP, 配置文件或者环境变量
- 登录日志 - 登录日志
- 链接日志 - 链接日志
- 文件传输日志 - 文件传输日志
- 快速使用web client - 快速使用web client
- i18n - i18n
- 通过 web client 分享给游客 - 通过 web client 分享给游客
- server控制(一些官方的简单的指令 [WIKI](https://github.com/lejianwen/rustdesk-api/wiki/Rustdesk-Command))
- Web Client - Web Client
- 自动获取API server - 自动获取API server
- 自动获取ID服务器和KEY - 自动获取ID服务器和KEY
@@ -43,98 +49,69 @@
- CLI - CLI
- 重置管理员密码 - 重置管理员密码
## 使用前准备
### [Rustdesk](https://github.com/rustdesk/rustdesk)
#### PC客户端使用的是 ***1.3.0***,经测试 ***1.2.6+*** 都可以
#### 关于PC端链接超时或者链接不上的问题以及解决方案
##### 链接不上或者超时
因为server端相对于客户端落后版本server不会响应客户端的`secure_tcp`请求,所以客户端超时。
相关代码代码位置在`https://github.com/rustdesk/rustdesk/blob/master/src/client.rs#L322`
```rust
if !key.is_empty() && !token.is_empty() {
// mainly for the security of token
allow_err!(secure_tcp(&mut socket, key).await);
}
```
可看到当`key`和`token`都不为空时,会调用`secure_tcp`但是server端不会响应所以客户端超时
`secure_tcp` 代码位置在 `https://github.com/rustdesk/rustdesk/blob/master/src/common.rs#L1203`
##### 4种解决方案
1. server端指定key。
- 优点:简单
- 缺点:链接不是加密的
```bash
hbbs -r <relay-server-ip[:port]> -k <key>
hbbr -k <key>
```
比如
```bash
hbbs -r <relay-server-ip[:port]> -k abc1234567
hbbr -k abc1234567
```
2. server端使用系统生成的key或者自定义的密钥对但如果client已登录链接时容易超时或者链接不上可以退出登录后再链接就可以了webclient可以不用退出登录
- 优点:链接加密
- 缺点:操作麻烦
3. server端使用系统生成的key或者自定义的密钥对fork官方客户端的代码将`secure_tcp`修改成直接返回,然后通过`Github Actions`编译,下载编译后的客户端。
参考[官方文档](https://rustdesk.com/docs/en/dev/build/all/)
- 优点:链接加密,可以自定义客户端一些功能,编译后直接可用
- 缺点需要自己fork代码编译有点难度
4. 使用[我fork的代码](https://github.com/lejianwen/rustdesk),已经修改了`secure_tcp`,可以直接下载使用,[下载地址](https://github.com/lejianwen/rustdesk/releases)
- 优点:代码改动可查看,`Github Actions`编译,链接加密,直接下载使用
- 缺点:可能跟不上官方版本更新
***对链接加密要求不高的可以使用`1`,对链接加密要求高的可以使用`3`或`4`***
## 功能 ## 功能
### API 服务 ### API 服务
基本实现了PC端基础的接口。支持Personal版本接口可以通过配置文件`rustdesk.personal`或环境变量`RUSTDESK_API_RUSTDESK_PERSONAL`来控制是否启用 基本实现了PC端基础的接口。支持Personal版本接口可以通过配置文件`rustdesk.personal`或环境变量`RUSTDESK_API_RUSTDESK_PERSONAL`来控制是否启用
#### 登录 <table>
<tr>
- 添加了`github`, `google` 以及`OIDC`授权登录需要在后台配置好就可以用了具体可看后台OAuth配置 <td width="50%" align="center" colspan="2"><b>登录</b></td>
- 添加了web后台授权登录,点击后直接登录后台就自动登录客户端了 </tr>
<tr>
![pc_login](docs/pc_login.png) <td width="50%" align="center" colspan="2"><img src="docs/pc_login.png"></td>
</tr>
#### 地址簿 <tr>
<td width="50%" align="center"><b>地址簿</b></td>
![pc_ab](docs/pc_ab.png) <td width="50%" align="center"><b>群组</b></td>
</tr>
#### 群组 <tr>
群组分为`共享组`和`普通组`,共享组中所有人都能看到小组成员的设备,普通组只有管理员能看到所有小组成员的设备 <td width="50%" align="center"><img src="docs/pc_ab.png"></td>
<td width="50%" align="center"><img src="docs/pc_gr.png"></td>
![pc_gr](docs/pc_gr.png) </tr>
</table>
### Web Admin: ### Web Admin:
* 使用前后端分离,提供用户友好的管理界面,主要用来管理和展示。前端代码在[rustdesk-api-web](https://github.com/lejianwen/rustdesk-api-web) * 使用前后端分离,提供用户友好的管理界面,主要用来管理和展示。前端代码在[rustdesk-api-web](https://github.com/lejianwen/rustdesk-api-web)
* 后台访问地址是`http://<your server>[:port]/_admin/`初次安装管理员为用户名密码为`admin` `admin`,请即时更改密码 * 后台访问地址是`http://<your server>[:port]/_admin/`
* 初次安装管理员为用户名为`admin`,密码将在控制台打印,可以通过[命令行](#CLI)更改密码
![img.png](./docs/init_admin_pwd.png)
1. 管理员界面 1. 管理员界面
![web_admin](docs/web_admin.png) ![web_admin](docs/web_admin.png)
2. 普通用户界面 2. 普通用户界面
![web_user](docs/web_admin_user.png) ![web_user](docs/web_admin_user.png)
右上角可以更改密码,可以切换语言,可以切换`白天/黑夜`模式
![web_resetpwd](docs/web_resetpwd.png)
3. 分组可以自定义,方便管理,暂时支持两种类型: `共享组` 和 `普通组` 3. 每个用户可以多个地址簿,也可以将地址簿共享给其他用户
![web_admin_gr](docs/web_admin_gr.png) 4. 分组可以自定义,方便管理,暂时支持两种类型: `共享组``普通组`
4. 可以直接打开webclient方便使用也可以分享给游客游客可以直接通过webclient远程到设备 5. 可以直接打开webclient方便使用也可以分享给游客游客可以直接通过webclient远程到设备
6. Oauth,支持了`Github`, `Google` 以及 `OIDC`, 需要创建一个`OAuth App`,然后配置到后台
![web_webclient](docs/admin_webclient.png)
5. Oauth,支持了`Github`, `Google` 以及 `OIDC`, 需要创建一个`OAuth App`,然后配置到后台
![web_admin_oauth](docs/web_admin_oauth.png)
- 对于`Google``Github`, `Issuer``Scopes`不需要填写. - 对于`Google``Github`, `Issuer``Scopes`不需要填写.
- 对于`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/oauth/callback`
,比如`http://127.0.0.1:21114/api/oauth/callback` ,比如`http://127.0.0.1:21114/api/oauth/callback`
7. 登录日志
8. 链接日志
9. 文件传输日志
10. server控制
- `简易模式`,已经界面化了一些简单的指令,可以直接在后台执行
![rustdesk_command_simple](./docs/rustdesk_command_simple.png)
- `高级模式`,直接在后台执行指令
* 可以官方指令
* 可以添加自定义指令
* 可以执行自定义指令
![rustdesk_command_advance](./docs/rustdesk_command_advance.png)
11. **LDAP 支持**, 当在API Server上设置了LDAP(已测试AD和LDAP),可以通过LDAP中的用户信息进行登录 https://github.com/lejianwen/rustdesk-api/issues/114 ,如果LDAP验证失败返回本地用户
### Web Client: ### Web Client:
@@ -154,6 +131,7 @@
![api_swag](docs/api_swag.png) ![api_swag](docs/api_swag.png)
### CLI ### CLI
```bash ```bash
# 查看帮助 # 查看帮助
./apimain -h ./apimain -h
@@ -205,43 +183,73 @@ logger:
proxy: proxy:
enable: false enable: false
host: "" 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.
``` ```
### 环境变量 ### 环境变量
变量名前缀是`RUSTDESK_API`,环境变量如果存在将覆盖配置文件中的配置 环境变量和配置文件`conf/config.yaml`中的配置一一对应,变量名前缀是`RUSTDESK_API`
下面表格并未全部列出,可以参考`conf/config.yaml`中的配置。
| 变量名 | 说明 | 示例 | | 变量名 | 说明 | 示例 |
|------------------------------------|---------------------------------------------------------|------------------------------| |--------------------------------------------------------|--------------------------------------------------------------------------------|------------------------------|
| TZ | 时区 | Asia/Shanghai | | TZ | 时区 | Asia/Shanghai |
| RUSTDESK_API_LANG | 语言 | `en`,`zh-CN` | | RUSTDESK_API_LANG | 语言 | `en`,`zh-CN` |
| RUSTDESK_API_APP_WEB_CLIENT | 是否启用web-client; 1:启用,0:不启用; 默认启用 | 1 | | RUSTDESK_API_APP_WEB_CLIENT | 是否启用web-client; 1:启用,0:不启用; 默认启用 | 1 |
| RUSTDESK_API_APP_REGISTER | 是否开启注册; `true`, `false` 默认`false` | `false` | | RUSTDESK_API_APP_REGISTER | 是否开启注册; `true`, `false` 默认`false` | `false` |
| RUSTDESK_API_APP_SHOW_SWAGGER | 是否可见swagger文档;`1`显示,`0`不显示,默认`0`不显示 | `1` | | RUSTDESK_API_APP_SHOW_SWAGGER | 是否可见swagger文档;`1`显示,`0`不显示,默认`0`不显示 | `1` |
| -----ADMIN配置----- | ---------- | ---------- | | RUSTDESK_API_APP_TOKEN_EXPIRE | token有效时长 | `3600` |
| RUSTDESK_API_ADMIN_TITLE | 后台标题 | `RustDesk Api Admin` | | -----ADMIN配置----- | ---------- | ---------- |
| RUSTDESK_API_ADMIN_HELLO | 后台欢迎语,可以使用`html` | | | RUSTDESK_API_ADMIN_TITLE | 后台标题 | `RustDesk Api Admin` |
| RUSTDESK_API_ADMIN_HELLO_FILE | 后台欢迎语文件,如果内容多,使用文件更方便。<br>会覆盖`RUSTDESK_API_ADMIN_HELLO` | `./conf/admin/hello.html` | | RUSTDESK_API_ADMIN_HELLO | 后台欢迎语,可以使用`html` | |
| -----GIN配置----- | ---------- | ---------- | | RUSTDESK_API_ADMIN_HELLO_FILE | 后台欢迎语文件,如果内容多,使用文件更方便。<br>会覆盖`RUSTDESK_API_ADMIN_HELLO` | `./conf/admin/hello.html` |
| RUSTDESK_API_GIN_TRUST_PROXY | 信任的代理IP列表以`,`分割,默认信任所有 | 192.168.1.2,192.168.1.3 | | -----GIN配置----- | ---------- | ---------- |
| -----------GORM配置---------------- | ------------------------------------ | --------------------------- | | RUSTDESK_API_GIN_TRUST_PROXY | 信任的代理IP列表`,`分割,默认信任所有 | 192.168.1.2,192.168.1.3 |
| RUSTDESK_API_GORM_TYPE | 数据库类型sqlite或者mysql默认sqlite | sqlite | | -----GORM配置----- | ---------- | --------------------------- |
| RUSTDESK_API_GORM_MAX_IDLE_CONNS | 数据库最大空闲连接数 | 10 | | RUSTDESK_API_GORM_TYPE | 数据库类型sqlite或者mysql默认sqlite | sqlite |
| RUSTDESK_API_GORM_MAX_OPEN_CONNS | 数据库最大打开连接数 | 100 | | RUSTDESK_API_GORM_MAX_IDLE_CONNS | 数据库最大空闲连接数 | 10 |
| RUSTDESK_API_RUSTDESK_PERSONAL | 是否启用个人版API 1:启用,0:不启用; 默认启用 | 1 | | RUSTDESK_API_GORM_MAX_OPEN_CONNS | 数据库最大打开连接数 | 100 |
| -----MYSQL配置----- | ---------- | ---------- | | RUSTDESK_API_RUSTDESK_PERSONAL | 是否启用个人版API 1:启用,0:不启用; 默认启用 | 1 |
| RUSTDESK_API_MYSQL_USERNAME | mysql用户名 | root | | -----MYSQL配置----- | ---------- | ---------- |
| RUSTDESK_API_MYSQL_PASSWORD | mysql密码 | 111111 | | RUSTDESK_API_MYSQL_USERNAME | mysql用户名 | root |
| RUSTDESK_API_MYSQL_ADDR | mysql地址 | 192.168.1.66:3306 | | RUSTDESK_API_MYSQL_PASSWORD | mysql密码 | 111111 |
| RUSTDESK_API_MYSQL_DBNAME | mysql数据库名 | rustdesk | | RUSTDESK_API_MYSQL_ADDR | mysql地址 | 192.168.1.66:3306 |
| -----RUSTDESK配置----- | --------------- | ---------- | | RUSTDESK_API_MYSQL_DBNAME | mysql数据库名 | rustdesk |
| RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk的id服务器地址 | 192.168.1.66:21116 | | -----RUSTDESK配置----- | ---------- | ---------- |
| RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk的relay服务器地址 | 192.168.1.66:21117 | | RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk的id服务器地址 | 192.168.1.66:21116 |
| RUSTDESK_API_RUSTDESK_API_SERVER | Rustdesk的api服务器地址 | http://192.168.1.66:21114 | | RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk的relay服务器地址 | 192.168.1.66:21117 |
| RUSTDESK_API_RUSTDESK_KEY | Rustdesk的key | 123456789 | | RUSTDESK_API_RUSTDESK_API_SERVER | Rustdesk的api服务器地址 | http://192.168.1.66:21114 |
| RUSTDESK_API_RUSTDESK_KEY_FILE | Rustdesk存放key的文件 | `./conf/data/id_ed25519.pub` | | RUSTDESK_API_RUSTDESK_KEY | Rustdeskkey | 123456789 |
| ----PROXY配置----- | --------------- | ---------- | | RUSTDESK_API_RUSTDESK_KEY_FILE | Rustdesk存放key的文件 | `./conf/data/id_ed25519.pub` |
| RUSTDESK_API_PROXY_ENABLE | 是否启用代理:`false`, `true` | `false` | | RUSTDESK_API_RUSTDESK_WEBCLIENT<br/>_MAGIC_QUERYONLINE | Web client v2 中是否启用新的在线状态查询方法; `1`:启用,`0`:不启用,默认不启用 | `0` |
| RUSTDESK_API_PROXY_HOST | 代理地址 | `http://127.0.0.1:1080` | | ----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 |
### 运行 ### 运行
@@ -307,7 +315,61 @@ proxy:
6. 打开浏览器访问`http://<your server[:port]>/_admin/`,默认用户名密码为`admin`,请及时更改密码。 6. 打开浏览器访问`http://<your server[:port]>/_admin/`,默认用户名密码为`admin`,请及时更改密码。
#### 使用`lejianwen/server-s6`镜像运行
- 已解决链接超时问题
- 可以强制登录后才能发起链接
- github https://github.com/lejianwen/rustdesk-server
```yaml
networks:
rustdesk-net:
external: false
services:
rustdesk:
ports:
- 21114:21114
- 21115:21115
- 21116:21116
- 21116:21116/udp
- 21117:21117
- 21118:21118
- 21119:21119
image: lejianwen/rustdesk-server-s6:latest
environment:
- RELAY=<relay_server[:port]>
- ENCRYPTED_ONLY=1
- MUST_LOGIN=N
- TZ=Asia/Shanghai
- RUSTDESK_API_RUSTDESK_ID_SERVER=<id_server[:21116]>
- RUSTDESK_API_RUSTDESK_RELAY_SERVER=<relay_server[:21117]>
- RUSTDESK_API_RUSTDESK_API_SERVER=http://<api_server[:21114]>
- RUSTDESK_API_KEY_FILE=/data/id_ed25519.pub
- RUSTDESK_API_JWT_KEY=xxxxxx # jwt key
volumes:
- /data/rustdesk/server:/data
- /data/rustdesk/api:/app/data #将数据库挂载
networks:
- rustdesk-net
restart: unless-stopped
```
## 其他 ## 其他
- [WIKI](https://github.com/lejianwen/rustdesk-api/wiki)
- [链接超时问题](https://github.com/lejianwen/rustdesk-api/issues/92)
- [修改客户端ID](https://github.com/abdullah-erturk/RustDesk-ID-Changer) - [修改客户端ID](https://github.com/abdullah-erturk/RustDesk-ID-Changer)
- [webclient来源](https://hub.docker.com/r/keyurbhole/flutter_web_desk) - [webclient来源](https://hub.docker.com/r/keyurbhole/flutter_web_desk)
## 鸣谢
感谢所有做过贡献的人!
<a href="https://github.com/lejianwen/rustdesk-api/graphs/contributors">
<img src="https://contrib.rocks/image?repo=lejianwen/rustdesk-api" />
</a>
## 感谢你的支持!如果这个项目对你有帮助,请点个⭐️鼓励一下,谢谢!

View File

@@ -8,6 +8,7 @@ desktop software that provides self-hosted solutions.
<img src="https://img.shields.io/badge/gin-v1.9.0-lightBlue"/> <img src="https://img.shields.io/badge/gin-v1.9.0-lightBlue"/>
<img src="https://img.shields.io/badge/gorm-v1.25.7-green"/> <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/swag-v1.16.3-yellow"/>
<img src="https://img.shields.io/badge/i18n-7-green"/>
<img src="https://github.com/lejianwen/rustdesk-api/actions/workflows/build.yml/badge.svg"/> <img src="https://github.com/lejianwen/rustdesk-api/actions/workflows/build.yml/badge.svg"/>
</div> </div>
@@ -18,7 +19,10 @@ desktop software that provides self-hosted solutions.
- Login - Login
- Address Book - Address Book
- Groups - Groups
- Authorized login, supports `GitHub`, `Google` and `OIDC` login, supports `web admin` authorized login - Authorized login,
- supports `GitHub`, `Google` and `OIDC` login,
- supports `web admin` authorized login,
- supports LDAP(test AD and openladp) if API Server config
- i18n - i18n
- Web Admin - Web Admin
- User Management - User Management
@@ -27,12 +31,14 @@ desktop software that provides self-hosted solutions.
- Tag Management - Tag Management
- Group Management - Group Management
- OAuth Management - OAuth Management
- LDAP Config by config file or ENV
- Login Logs - Login Logs
- Connection Logs - Connection Logs
- File Transfer Logs - File Transfer Logs
- Quick access to web client - Quick access to web client
- i18n - i18n
- Share to guest by web client - Share to guest by web client
- Server control (some simple official commands [WIKI](https://github.com/lejianwen/rustdesk-api/wiki/Rustdesk-Command))
- Web Client - Web Client
- Automatically obtain API server - Automatically obtain API server
- Automatically obtain ID server and KEY - Automatically obtain ID server and KEY
@@ -40,105 +46,73 @@ desktop software that provides self-hosted solutions.
- Visitors are remotely to the device via a temporary sharing link - Visitors are remotely to the device via a temporary sharing link
- CLI - CLI
- Reset admin password - Reset admin password
## Prerequisites
### [Rustdesk](https://github.com/rustdesk/rustdesk)
#### The PC client uses version ***1.3.0***, and versions ***1.2.6+*** have been tested to work.
#### Solutions for PC client connection timeout or connection issues
##### Connection issues or timeouts
Because the server version lags behind the client version, the server does not respond to the client's `secure_tcp` request, causing the client to timeout.
Relevant code can be found at `https://github.com/rustdesk/rustdesk/blob/master/src/client.rs#L322`
```rust
if !key.is_empty() && !token.is_empty() {
// mainly for the security of token
allow_err!(secure_tcp(&mut socket, key).await);
}
```
As seen, when both `key` and `token` are not empty, `secure_tcp` is called, but the server does not respond, causing the client to timeout.
The `secure_tcp` code is located at `https://github.com/rustdesk/rustdesk/blob/master/src/common.rs#L1203`
##### Four Solutions
1. Specify the key on the server.
- Advantage: Simple
- Disadvantage: The connection is not encrypted
```bash
hbbs -r <relay-server-ip[:port]> -k <key>
hbbr -k <key>
```
For example
```bash
hbbs -r <relay-server-ip[:port]> -k abc1234567
hbbr -k abc1234567
```
2. Use a system-generated key or a custom key pair on the server. If the client is already logged in, it may timeout or fail to connect. Logging out and reconnecting usually resolves the issue, and the web client does not need to log out.
- Advantage: Encrypted connection
- Disadvantage: Complicated operation
3. Use a system-generated key or a custom key pair on the server, fork the official client code to modify `secure_tcp` to return directly, then compile using `Github Actions` and download the compiled client.
Refer to [official documentation](https://rustdesk.com/docs/en/dev/build/all/)
- Advantage: Encrypted connection, customizable client features, ready to use after compilation
- Disadvantage: Requires forking code and compiling, which can be challenging
4. Use [my forked code](https://github.com/lejianwen/rustdesk), which has already modified `secure_tcp`. You can download and use it directly from [here](https://github.com/lejianwen/rustdesk/releases)
- Advantage: Code changes are viewable, compiled with `Github Actions`, encrypted connection, ready to use
- Disadvantage: May not keep up with official version updates
***If encryption is not a high priority, use `1`. If encryption is important, use `3` or `4`.***
## Overview ## Overview
### API Service ### API Service
Basic implementation of the PC client's primary interfaces.Supports the Personal version api, which can be enabled by configuring the `rustdesk.personal` file or the `RUSTDESK_API_RUSTDESK_PERSONAL` environment variable. Basic implementation of the PC client's primary interfaces.Supports the Personal version api, which can be enabled by configuring the `rustdesk.personal` file or the `RUSTDESK_API_RUSTDESK_PERSONAL` environment variable.
#### Login <table>
<tr>
- Added `GitHub`, `Google` and `OIDC` login, which can be used after configuration in the admin panel. See the OAuth <td width="50%" align="center" colspan="2"><b>Login</b></td>
configuration section for details. </tr>
- Added authorization login for the web admin panel. <tr>
<td width="50%" align="center" colspan="2"><img src="docs/en_img/pc_login.png"></td>
![pc_login](docs/en_img/pc_login.png) </tr>
<tr>
#### Address Book <td width="50%" align="center"><b>Address Book</b></td>
<td width="50%" align="center"><b>Groups</b></td>
![pc_ab](docs/en_img/pc_ab.png) </tr>
<tr>
#### Groups <td width="50%" align="center"><img src="docs/en_img/pc_ab.png"></td>
Groups are divided into `shared groups` and `regular groups`. In shared groups, everyone can see the peers of all group members, while in regular groups, only administrators can see all members' peers. <td width="50%" align="center"><img src="docs/en_img/pc_gr.png"></td>
</tr>
![pc_gr](docs/en_img/pc_gr.png) </table>
### Web Admin ### Web Admin
* The frontend and backend are separated to provide a user-friendly management interface, primarily for managing and * The frontend and backend are separated to provide a user-friendly management interface, primarily for managing and
displaying data.Frontend code is available at [rustdesk-api-web](https://github.com/lejianwen/rustdesk-api-web) displaying data.Frontend code is available at [rustdesk-api-web](https://github.com/lejianwen/rustdesk-api-web)
* Admin panel URL: `http://<your server[:port]>/_admin/`. The default username and password for the initial * Admin panel URL: `http://<your server[:port]>/_admin/`
installation are `admin` `admin`, please change the password immediately. * For the initial installation, the admin username is `admin`, and the password will be printed in the console. You can change the password via the [command line](#CLI).
![img.png](./docs/init_admin_pwd.png)
1. Admin interface: 1. Admin interface:
![web_admin](docs/en_img/web_admin.png) ![web_admin](docs/en_img/web_admin.png)
2. Regular user interface: 2. Regular user interface:
![web_user](docs/en_img/web_admin_user.png) ![web_user](docs/en_img/web_admin_user.png)
In the top right corner, you can change the password, switch languages, and toggle between `day/night` mode.
![web_resetpwd](docs/en_img/web_resetpwd.png) 3. Each user can have multiple address books, which can also be shared with other users.
3. Groups can be customized for easy management. Currently, two types are supported: `shared group` and `regular group`. 4. Groups can be customized for easy management. Currently, two types are supported: `shared group` and `regular group`.
![web_admin_gr](docs/en_img/web_admin_gr.png) 5. You can directly launch the client or open the web client for convenience; you can also share it with guests, who can remotely access the device via the web client.
4. You can directly launch the client or open the web client for convenience; you can also share it with guests, who can remotely access the device via the web client. 6. OAuth support: Currently, `GitHub`, `Google` and `OIDC` are supported. You need to create an `OAuth App` and configure it in
![web_webclient](docs/en_img/admin_webclient.png)
5. OAuth support: Currently, `GitHub`, `Google` and `OIDC` are supported. You need to create an `OAuth App` and configure it in
the admin panel. the admin panel.
![web_admin_oauth](docs/en_img/web_admin_oauth.png)
- For `Google` and `Github`, you don't need to fill the `Issuer` and `Scpoes` - For `Google` and `Github`, you don't need to fill the `Issuer` and `Scpoes`
- For `OIDC`, you must set the `Issuer`. And `Scopes` is optional which default is `openid,email,profile`, please make sure this `Oauth App` can access `sub`, `email` and `preferred_username` - 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/oauth/callback`,
e.g., `http://127.0.0.1:21114/api/oauth/callback`. e.g., `http://127.0.0.1:21114/api/oauth/callback`.
7. Login logs
8. Connection logs
9. File transfer logs
10. Server control
- `Simple mode`, some simple commands have been GUI-ized and can be executed directly in the backend
![rustdesk_command_simple](./docs/en_img/rustdesk_command_simple.png)
- `Advanced mode`, commands can be executed directly in the backend
* Official commands can be used
* Custom commands can be added
* Custom commands can be executed
![rustdesk_command_advance](./docs/en_img/rustdesk_command_advance.png)
11. **LDAP Support**, When you setup the LDAP(test for OpenLDAP and AD), you can login with the LDAP's user. https://github.com/lejianwen/rustdesk-api/issues/114 , if LDAP fail fallback local user
### Web Client: ### Web Client:
1. If you're already logged into the admin panel, the web client will log in automatically. 1. If you're already logged into the admin panel, the web client will log in automatically.
@@ -208,43 +182,73 @@ logger:
proxy: proxy:
enable: false enable: false
host: "" 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 ### Environment Variables
The prefix for variable names is `RUSTDESK_API`. If environment variables exist, they will override the configurations in the configuration file. The environment variables correspond one-to-one with the configurations in the `conf/config.yaml` file. The prefix for variable names is `RUSTDESK_API`.
The table below does not list all configurations. Please refer to the configurations in `conf/config.yaml`.
| Variable Name | Description | Example | | Variable Name | Description | Example |
|------------------------------------|-------------------------------------------------------------------------|-------------------------------| |---------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------|
| TZ | timezone | Asia/Shanghai | | TZ | timezone | Asia/Shanghai |
| RUSTDESK_API_LANG | Language | `en`,`zh-CN` | | 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_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_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_SHOW_SWAGGER | swagger visible; 1: yes, 0: no; default: 0 | `0` |
| ----- ADMIN Configuration----- | ---------- | ---------- | | RUSTDESK_API_APP_TOKEN_EXPIRE | token expire duration(second) | `3600` |
| RUSTDESK_API_ADMIN_TITLE | Admin Title | `RustDesk Api Admin` | | ----- ADMIN Configuration----- | ---------- | ---------- |
| RUSTDESK_API_ADMIN_HELLO | Admin welcome message, you can use `html` | | | RUSTDESK_API_ADMIN_TITLE | Admin Title | `RustDesk Api Admin` |
| RUSTDESK_API_ADMIN_HELLO_FILE | Admin welcome message file,<br>will override `RUSTDESK_API_ADMIN_HELLO` | `./conf/admin/hello.html` | | RUSTDESK_API_ADMIN_HELLO | Admin welcome message, you can use `html` | |
| ----- GIN Configuration ----- | --------------------------------------- | ----------------------------- | | RUSTDESK_API_ADMIN_HELLO_FILE | Admin welcome message file,<br>will override `RUSTDESK_API_ADMIN_HELLO` | `./conf/admin/hello.html` |
| RUSTDESK_API_GIN_TRUST_PROXY | Trusted proxy IPs, separated by commas. | 192.168.1.2,192.168.1.3 | | ----- GIN Configuration ----- | --------------------------------------- | ----------------------------- |
| ----- GORM Configuration ----- | --------------------------------------- | ----------------------------- | | RUSTDESK_API_GIN_TRUST_PROXY | Trusted proxy IPs, separated by commas. | 192.168.1.2,192.168.1.3 |
| RUSTDESK_API_GORM_TYPE | Database type (`sqlite` or `mysql`). Default is `sqlite`. | sqlite | | ----- GORM Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_GORM_MAX_IDLE_CONNS | Maximum idle connections | 10 | | RUSTDESK_API_GORM_TYPE | Database type (`sqlite` or `mysql`). Default is `sqlite`. | sqlite |
| RUSTDESK_API_GORM_MAX_OPEN_CONNS | Maximum open connections | 100 | | RUSTDESK_API_GORM_MAX_IDLE_CONNS | Maximum idle connections | 10 |
| RUSTDESK_API_RUSTDESK_PERSONAL | Open Personal Api 1:Enable,0:Disable | 1 | | RUSTDESK_API_GORM_MAX_OPEN_CONNS | Maximum open connections | 100 |
| ----- MYSQL Configuration ----- | --------------------------------------- | ----------------------------- | | RUSTDESK_API_RUSTDESK_PERSONAL | Open Personal Api 1:Enable,0:Disable | 1 |
| RUSTDESK_API_MYSQL_USERNAME | MySQL username | root | | ----- MYSQL Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_MYSQL_PASSWORD | MySQL password | 111111 | | RUSTDESK_API_MYSQL_USERNAME | MySQL username | root |
| RUSTDESK_API_MYSQL_ADDR | MySQL address | 192.168.1.66:3306 | | RUSTDESK_API_MYSQL_PASSWORD | MySQL password | 111111 |
| RUSTDESK_API_MYSQL_DBNAME | MySQL database name | rustdesk | | RUSTDESK_API_MYSQL_ADDR | MySQL address | 192.168.1.66:3306 |
| ----- RUSTDESK Configuration ----- | --------------------------------------- | ----------------------------- | | RUSTDESK_API_MYSQL_DBNAME | MySQL database name | rustdesk |
| RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk ID server address | 192.168.1.66:21116 | | ----- RUSTDESK Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk relay server address | 192.168.1.66:21117 | | RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk ID server address | 192.168.1.66:21116 |
| RUSTDESK_API_RUSTDESK_API_SERVER | Rustdesk API server address | http://192.168.1.66:21114 | | RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk relay server address | 192.168.1.66:21117 |
| RUSTDESK_API_RUSTDESK_KEY | Rustdesk key | 123456789 | | RUSTDESK_API_RUSTDESK_API_SERVER | Rustdesk API server address | http://192.168.1.66:21114 |
| RUSTDESK_API_RUSTDESK_KEY_FILE | Rustdesk存放key的文件 | `./conf/data/id_ed25519.pub` | | RUSTDESK_API_RUSTDESK_KEY | Rustdesk key | 123456789 |
| ---- PROXY ----- | --------------- | ---------- | | RUSTDESK_API_RUSTDESK_KEY_FILE | Rustdesk key file | `./conf/data/id_ed25519.pub` |
| RUSTDESK_API_PROXY_ENABLE | proxy_enable :`false`, `true` | `false` | | 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_PROXY_HOST | proxy_host | `http://127.0.0.1:1080` | | ---- 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 |
### Installation Steps ### Installation Steps
@@ -313,8 +317,58 @@ Download the release from [release](https://github.com/lejianwen/rustdesk-api/re
6. Open your browser and visit `http://<your server[:port]>/_admin/`, with default credentials `admin admin`. Please 6. Open your browser and visit `http://<your server[:port]>/_admin/`, with default credentials `admin admin`. Please
change the password promptly. change the password promptly.
#### Running with my forked server-s6 image
- Connection timeout issue resolved
- Can enforce login before initiating a connection
- github https://github.com/lejianwen/rustdesk-server
```yaml
networks:
rustdesk-net:
external: false
services:
rustdesk:
ports:
- 21114:21114
- 21115:21115
- 21116:21116
- 21116:21116/udp
- 21117:21117
- 21118:21118
- 21119:21119
image: lejianwen/rustdesk-server-s6:latest
environment:
- RELAY=<relay_server[:port]>
- ENCRYPTED_ONLY=1
- MUST_LOGIN=N
- TZ=Asia/Shanghai
- RUSTDESK_API_RUSTDESK_ID_SERVER=<id_server[:21116]>
- RUSTDESK_API_RUSTDESK_RELAY_SERVER=<relay_server[:21117]>
- RUSTDESK_API_RUSTDESK_API_SERVER=http://<api_server[:21114]>
- RUSTDESK_API_KEY_FILE=/data/id_ed25519.pub
- RUSTDESK_API_JWT_KEY=xxxxxx # jwt key
volumes:
- /data/rustdesk/server:/data
- /data/rustdesk/api:/app/data #将数据库挂载
networks:
- rustdesk-net
restart: unless-stopped
```
## Others ## Others
- [WIKI](https://github.com/lejianwen/rustdesk-api/wiki)
- [Connection Timeout](https://github.com/lejianwen/rustdesk-api/issues/92)
- [Change client ID](https://github.com/abdullah-erturk/RustDesk-ID-Changer) - [Change client ID](https://github.com/abdullah-erturk/RustDesk-ID-Changer)
- [Web client source](https://hub.docker.com/r/keyurbhole/flutter_web_desk) - [Web client source](https://hub.docker.com/r/keyurbhole/flutter_web_desk)
## Acknowledgements
Thanks to everyone who contributed!
<a href="https://github.com/lejianwen/rustdesk-api/graphs/contributors">
<img src="https://contrib.rocks/image?repo=lejianwen/rustdesk-api" />
</a>
## Thanks for your support! If you find this project useful, please give it a ⭐️. Thank you!

View File

@@ -5,18 +5,20 @@ import (
"Gwen/global" "Gwen/global"
"Gwen/http" "Gwen/http"
"Gwen/lib/cache" "Gwen/lib/cache"
"Gwen/lib/jwt"
"Gwen/lib/lock" "Gwen/lib/lock"
"Gwen/lib/logger" "Gwen/lib/logger"
"Gwen/lib/orm" "Gwen/lib/orm"
"Gwen/lib/upload" "Gwen/lib/upload"
"Gwen/model" "Gwen/model"
"Gwen/service" "Gwen/service"
"fmt" "Gwen/utils"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/nicksnyder/go-i18n/v2/i18n" "github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"os" "os"
"strconv" "strconv"
"time"
) )
// @title 管理系统API // @title 管理系统API
@@ -37,7 +39,7 @@ var rootCmd = &cobra.Command{
InitGlobal() InitGlobal()
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
//gin global.Logger.Info("API SERVER START")
http.ApiInit() http.ApiInit()
}, },
} }
@@ -52,10 +54,10 @@ var resetPwdCmd = &cobra.Command{
admin := service.AllService.UserService.InfoById(1) admin := service.AllService.UserService.InfoById(1)
err := service.AllService.UserService.UpdatePassword(admin, pwd) err := service.AllService.UserService.UpdatePassword(admin, pwd)
if err != nil { if err != nil {
fmt.Printf("reset password fail! %v \n", err) global.Logger.Error("reset password fail! ", err)
return return
} }
fmt.Printf("reset password success! \n") global.Logger.Info("reset password success! ")
}, },
} }
var resetUserPwdCmd = &cobra.Command{ var resetUserPwdCmd = &cobra.Command{
@@ -68,20 +70,20 @@ var resetUserPwdCmd = &cobra.Command{
pwd := args[1] pwd := args[1]
uid, err := strconv.Atoi(userId) uid, err := strconv.Atoi(userId)
if err != nil { if err != nil {
fmt.Printf("userId must be int! \n") global.Logger.Warn("userId must be int!")
return return
} }
if uid <= 0 { if uid <= 0 {
fmt.Printf("userId must be greater than 0! \n") global.Logger.Warn("userId must be greater than 0! ")
return return
} }
u := service.AllService.UserService.InfoById(uint(uid)) u := service.AllService.UserService.InfoById(uint(uid))
err = service.AllService.UserService.UpdatePassword(u, pwd) err = service.AllService.UserService.UpdatePassword(u, pwd)
if err != nil { if err != nil {
fmt.Printf("reset password fail! %v \n", err) global.Logger.Warn("reset password fail! ", err)
return return
} }
fmt.Printf("reset password success! \n") global.Logger.Info("reset password success!")
}, },
} }
@@ -91,7 +93,7 @@ func init() {
} }
func main() { func main() {
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
fmt.Println(err) global.Logger.Error(err)
os.Exit(1) os.Exit(1)
} }
} }
@@ -100,9 +102,6 @@ func InitGlobal() {
//配置解析 //配置解析
global.Viper = config.Init(&global.Config, global.ConfigPath) global.Viper = config.Init(&global.Config, global.ConfigPath)
//从配置文件中加载密钥
config.LoadKeyFile(&global.Config.Rustdesk)
//日志 //日志
global.Logger = logger.New(&logger.Config{ global.Logger = logger.New(&logger.Config{
Path: global.Config.Logger.Path, Path: global.Config.Logger.Path,
@@ -163,20 +162,19 @@ func InitGlobal() {
//jwt //jwt
//fmt.Println(global.Config.Jwt.PrivateKey) //fmt.Println(global.Config.Jwt.PrivateKey)
//global.Jwt = jwt.NewJwt(global.Config.Jwt.PrivateKey, global.Config.Jwt.ExpireDuration*time.Second) global.Jwt = jwt.NewJwt(global.Config.Jwt.Key, global.Config.Jwt.ExpireDuration*time.Second)
//locker //locker
global.Lock = lock.NewLocal() global.Lock = lock.NewLocal()
} }
func DatabaseAutoUpdate() { func DatabaseAutoUpdate() {
version := 246 version := 260
db := global.DB db := global.DB
if global.Config.Gorm.Type == config.TypeMysql { if global.Config.Gorm.Type == config.TypeMysql {
//检查存不存在数据库,不存在则创建 //检查存不存在数据库,不存在则创建
dbName := db.Migrator().CurrentDatabase() dbName := db.Migrator().CurrentDatabase()
fmt.Println("dbName", dbName)
if dbName == "" { if dbName == "" {
dbName = global.Config.Mysql.Dbname dbName = global.Config.Mysql.Dbname
// 移除 DSN 中的数据库名称,以便初始连接时不指定数据库 // 移除 DSN 中的数据库名称,以便初始连接时不指定数据库
@@ -188,18 +186,18 @@ func DatabaseAutoUpdate() {
// 获取底层的 *sql.DB 对象,并确保在程序退出时关闭连接 // 获取底层的 *sql.DB 对象,并确保在程序退出时关闭连接
sqlDBWithoutDB, err := dbWithoutDB.DB() sqlDBWithoutDB, err := dbWithoutDB.DB()
if err != nil { if err != nil {
fmt.Printf("获取底层 *sql.DB 对象失败: %v\n", err) global.Logger.Error("获取底层 *sql.DB 对象失败: %v\n", err)
return return
} }
defer func() { defer func() {
if err := sqlDBWithoutDB.Close(); err != nil { if err := sqlDBWithoutDB.Close(); err != nil {
fmt.Printf("关闭连接失败: %v\n", err) global.Logger.Error("关闭连接失败: %v\n", err)
} }
}() }()
err = dbWithoutDB.Exec("CREATE DATABASE IF NOT EXISTS " + dbName + " DEFAULT CHARSET utf8mb4").Error err = dbWithoutDB.Exec("CREATE DATABASE IF NOT EXISTS " + dbName + " DEFAULT CHARSET utf8mb4").Error
if err != nil { if err != nil {
fmt.Println(err) global.Logger.Error(err)
return return
} }
} }
@@ -236,7 +234,7 @@ func DatabaseAutoUpdate() {
} }
func Migrate(version uint) { func Migrate(version uint) {
fmt.Println("migrating....", version) global.Logger.Info("migrating....", version)
err := global.DB.AutoMigrate( err := global.DB.AutoMigrate(
&model.Version{}, &model.Version{},
&model.User{}, &model.User{},
@@ -253,9 +251,10 @@ func Migrate(version uint) {
&model.AuditFile{}, &model.AuditFile{},
&model.AddressBookCollection{}, &model.AddressBookCollection{},
&model.AddressBookCollectionRule{}, &model.AddressBookCollectionRule{},
&model.ServerCmd{},
) )
if err != nil { if err != nil {
fmt.Println("migrate err :=>", err) global.Logger.Error("migrate err :=>", err)
} }
global.DB.Create(&model.Version{Version: version}) global.DB.Create(&model.Version{Version: version})
//如果是初次则创建一个默认用户 //如果是初次则创建一个默认用户
@@ -289,7 +288,11 @@ func Migrate(version uint) {
IsAdmin: &is_admin, IsAdmin: &is_admin,
GroupId: 1, GroupId: 1,
} }
admin.Password = service.AllService.UserService.EncryptPassword("admin")
// 生成随机密码
pwd := utils.RandomString(8)
global.Logger.Info("Admin Password Is: ", pwd)
admin.Password = service.AllService.UserService.EncryptPassword(pwd)
global.DB.Create(admin) global.DB.Create(admin)
} }

View File

@@ -1 +1 @@
👏👏👏 你好 <strong>{{username}}</strong>, 欢迎使用 <a href='https://github.com/lejianwen/rustdesk-api' target='_blank'>RustDesk Api Admin</a> ### 👏👏👏 你好 ***{{username}}*** 欢迎使用 [RustDesk Api](https://github.com/lejianwen/rustdesk-api)

View File

@@ -3,6 +3,8 @@ app:
web-client: 1 # 1:启用 0:禁用 web-client: 1 # 1:启用 0:禁用
register: false #是否开启注册 register: false #是否开启注册
show-swagger: 0 # 1:启用 0:禁用 show-swagger: 0 # 1:启用 0:禁用
token-expire: 360000
web-sso: true #web auth sso
admin: admin:
title: "RustDesk Api Admin" title: "RustDesk Api Admin"
hello-file: "./conf/admin/hello.html" #优先使用file hello-file: "./conf/admin/hello.html" #优先使用file
@@ -26,15 +28,40 @@ rustdesk:
relay-server: "192.168.1.66:21117" relay-server: "192.168.1.66:21117"
api-server: "http://127.0.0.1:21114" api-server: "http://127.0.0.1:21114"
key: "" key: ""
key-file: "./conf/data/id_ed25519.pub" key-file: "/data/id_ed25519.pub"
personal: 1 personal: 1
webclient-magic-queryonline: 0
logger: logger:
path: "./runtime/log.txt" path: "./runtime/log.txt"
level: "warn" #trace,debug,info,warn,error,fatal level: "info" #trace,debug,info,warn,error,fatal
report-caller: true report-caller: true
proxy: proxy:
enable: false enable: false
host: "http://127.0.0.1:1080" host: "http://127.0.0.1:1080"
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.
redis: redis:
addr: "127.0.0.1:6379" addr: "127.0.0.1:6379"
password: "" password: ""
@@ -52,6 +79,4 @@ oss:
callback-url: "" callback-url: ""
expire-time: 30 expire-time: 30
max-byte: 10240 max-byte: 10240
jwt:
private-key: "./conf/jwt_pri.pem"
expire-duration: 360000

View File

View File

@@ -2,7 +2,6 @@ package config
import ( import (
"fmt" "fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper" "github.com/spf13/viper"
"strings" "strings"
) )
@@ -17,6 +16,8 @@ type App struct {
WebClient int `mapstructure:"web-client"` WebClient int `mapstructure:"web-client"`
Register bool `mapstructure:"register"` Register bool `mapstructure:"register"`
ShowSwagger int `mapstructure:"show-swagger"` ShowSwagger int `mapstructure:"show-swagger"`
TokenExpire int `mapstructure:"token-expire"`
WebSso bool `mapstructure:"web-sso"`
} }
type Admin struct { type Admin struct {
Title string `mapstructure:"title"` Title string `mapstructure:"title"`
@@ -37,10 +38,11 @@ type Config struct {
Jwt Jwt Jwt Jwt
Rustdesk Rustdesk Rustdesk Rustdesk
Proxy Proxy Proxy Proxy
Ldap Ldap
} }
// Init 初始化配置 // Init 初始化配置
func Init(rowVal interface{}, path string) *viper.Viper { func Init(rowVal *Config, path string) *viper.Viper {
if path == "" { if path == "" {
path = DefaultConfig path = DefaultConfig
} }
@@ -54,18 +56,26 @@ func Init(rowVal interface{}, path string) *viper.Viper {
if err != nil { if err != nil {
panic(fmt.Errorf("Fatal error config file: %s \n", err)) panic(fmt.Errorf("Fatal error config file: %s \n", err))
} }
v.WatchConfig() /*
v.OnConfigChange(func(e fsnotify.Event) { v.WatchConfig()
//配置文件修改监听
fmt.Println("config file changed:", e.Name)
if err2 := v.Unmarshal(rowVal); err2 != nil { //监听配置修改没什么必要
fmt.Println(err2) v.OnConfigChange(func(e fsnotify.Event) {
} //配置文件修改监听
}) fmt.Println("config file changed:", e.Name)
if err2 := v.Unmarshal(rowVal); err2 != nil {
fmt.Println(err2)
}
rowVal.Rustdesk.LoadKeyFile()
rowVal.Rustdesk.ParsePort()
})
*/
if err := v.Unmarshal(rowVal); err != nil { if err := v.Unmarshal(rowVal); err != nil {
fmt.Println(err) fmt.Println(err)
} }
rowVal.Rustdesk.LoadKeyFile()
rowVal.Rustdesk.ParsePort()
return v return v
} }

View File

@@ -3,6 +3,6 @@ package config
import "time" import "time"
type Jwt struct { type Jwt struct {
PrivateKey string `mapstructure:"private-key"` Key string `mapstructure:"key"`
ExpireDuration time.Duration `mapstructure:"expire-duration"` ExpireDuration time.Duration `mapstructure:"expire-duration"`
} }

36
config/ldap.go Normal file
View File

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

View File

@@ -2,29 +2,56 @@ package config
import ( import (
"os" "os"
"strconv"
"strings"
)
const (
DefaultIdServerPort = 21116
DefaultRelayServerPort = 21117
) )
type Rustdesk struct { type Rustdesk struct {
IdServer string `mapstructure:"id-server"` IdServer string `mapstructure:"id-server"`
RelayServer string `mapstructure:"relay-server"` IdServerPort int `mapstructure:"-"`
ApiServer string `mapstructure:"api-server"` RelayServer string `mapstructure:"relay-server"`
Key string `mapstructure:"key"` RelayServerPort int `mapstructure:"-"`
KeyFile string `mapstructure:"key-file"` ApiServer string `mapstructure:"api-server"`
Personal int `mapstructure:"personal"` Key string `mapstructure:"key"`
KeyFile string `mapstructure:"key-file"`
Personal int `mapstructure:"personal"`
//webclient-magic-queryonline
WebclientMagicQueryonline int `mapstructure:"webclient-magic-queryonline"`
} }
func LoadKeyFile(rustdesk *Rustdesk) { func (rd *Rustdesk) LoadKeyFile() {
// Load key file // Load key file
if rustdesk.Key != "" { if rd.Key != "" {
return return
} }
if rustdesk.KeyFile != "" { if rd.KeyFile != "" {
// Load key from file // Load key from file
b, err := os.ReadFile(rustdesk.KeyFile) b, err := os.ReadFile(rd.KeyFile)
if err != nil { if err != nil {
return return
} }
rustdesk.Key = string(b) rd.Key = string(b)
return return
} }
} }
func (rd *Rustdesk) ParsePort() {
// Parse port
idres := strings.Split(rd.IdServer, ":")
if len(idres) == 1 {
rd.IdServerPort = DefaultIdServerPort
} else if len(idres) == 2 {
rd.IdServerPort, _ = strconv.Atoi(idres[1])
}
relayres := strings.Split(rd.RelayServer, ":")
if len(relayres) == 1 {
rd.RelayServerPort = DefaultRelayServerPort
} else if len(relayres) == 2 {
rd.RelayServerPort, _ = strconv.Atoi(relayres[1])
}
}

5
debian/changelog vendored Normal file
View File

@@ -0,0 +1,5 @@
rustdesk-api-server (1.3.6) UNRELEASED; urgency=medium
* Update the version to 1.3.6 to match the client.
-- rustdesk-api <ymwlpoolc@qq.com> Tue, 24 Dec 2024 13:48:34 +0800

1
debian/compat vendored Normal file
View File

@@ -0,0 +1 @@
10

13
debian/control.tpl vendored Normal file
View File

@@ -0,0 +1,13 @@
Source: rustdesk-api-server
Section: net
Priority: optional
Maintainer: ymwl <ymwlpoolc@qq.com>
Build-Depends: debhelper (>= 10), pkg-config
Standards-Version: 4.5.0
Homepage: https://github.com/lejianwen/rustdesk-api/
Package: rustdesk-api-server
Architecture: {{ ARCH }}
Depends: systemd ${misc:Depends}
Description: RustDesk api server
RustDesk api server, it is free and open source.

21
debian/copyright vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024-present Lejianwen and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

6
debian/rules vendored Normal file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/make -f
%:
dh $@
override_dh_builddeb:
dh_builddeb -- -Zgzip

6
debian/rustdesk-api-server.install vendored Normal file
View File

@@ -0,0 +1,6 @@
bin/rustdesk-api usr/bin
systemd/rustdesk-api.service lib/systemd/system
conf var/lib/rustdesk-api
data var/lib/rustdesk-api
resources var/lib/rustdesk-api
runtime var/lib/rustdesk-api

28
debian/rustdesk-api-server.postinst vendored Normal file
View File

@@ -0,0 +1,28 @@
#!/bin/sh
set -e
SERVICE=rustdesk-api.service
if [ "$1" = "configure" ]; then
mkdir -p /var/log/rustdesk-api
fi
case "$1" in
configure|abort-upgrade|abort-deconfigure|abort-remove)
mkdir -p /var/lib/rustdesk-api/
deb-systemd-helper unmask "${SERVICE}" >/dev/null || true
if deb-systemd-helper --quiet was-enabled "${SERVICE}"; then
deb-systemd-invoke enable "${SERVICE}" >/dev/null || true
else
deb-systemd-invoke update-state "${SERVICE}" >/dev/null || true
fi
systemctl --system daemon-reload >/dev/null || true
if [ -n "$2" ]; then
deb-systemd-invoke restart "${SERVICE}" >/dev/null || true
else
deb-systemd-invoke start "${SERVICE}" >/dev/null || true
fi
;;
esac
exit 0

18
debian/rustdesk-api-server.postrm vendored Normal file
View File

@@ -0,0 +1,18 @@
#!/bin/sh
set -e
SERVICE=rustdesk-api.service
systemctl --system daemon-reload >/dev/null || true
if [ "$1" = "purge" ]; then
rm -rf /var/log/rustdesk-api/rustdesk-api.*
deb-systemd-helper purge "${SERVICE}" >/dev/null || true
deb-systemd-helper unmask "${SERVICE}" >/dev/null || true
fi
if [ "$1" = "remove" ]; then
deb-systemd-helper mask "${SERVICE}" >/dev/null || true
fi
exit 0

13
debian/rustdesk-api-server.prerm vendored Normal file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
set -e
SERVICE=rustdesk-api.service
case "$1" in
remove|deconfigure)
deb-systemd-invoke stop "${SERVICE}" >/dev/null || true
deb-systemd-invoke disable "${SERVICE}" >/dev/null || true
;;
esac
exit 0

1
debian/source/format vendored Normal file
View File

@@ -0,0 +1 @@
3.0 (native)

View File

@@ -981,40 +981,6 @@ const docTemplateadmin = `{
} }
} }
}, },
"/admin/app-config": {
"get": {
"security": [
{
"token": []
}
],
"description": "APP服务配置",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"ADMIN"
],
"summary": "APP服务配置",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/audit_conn/batchDelete": { "/admin/audit_conn/batchDelete": {
"post": { "post": {
"security": [ "security": [
@@ -1881,7 +1847,7 @@ const docTemplateadmin = `{
} }
} }
}, },
"/admin/login_log/delete": { "/admin/login_log/batchDelete": {
"post": { "post": {
"security": [ "security": [
{ {
@@ -1926,6 +1892,51 @@ const docTemplateadmin = `{
} }
} }
}, },
"/admin/login_log/delete": {
"post": {
"security": [
{
"token": []
}
],
"description": "登录日志删除",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"登录日志"
],
"summary": "登录日志删除",
"parameters": [
{
"description": "登录日志信息",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/model.LoginLog"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/login_log/detail/{id}": { "/admin/login_log/detail/{id}": {
"get": { "get": {
"security": [ "security": [
@@ -2757,6 +2768,162 @@ const docTemplateadmin = `{
} }
} }
}, },
"/admin/my/login_log/batchDelete": {
"post": {
"security": [
{
"token": []
}
],
"description": "登录日志批量删除",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"我的登录日志"
],
"summary": "登录日志批量删除",
"parameters": [
{
"description": "登录日志",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/admin.LoginLogIds"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/my/login_log/delete": {
"post": {
"security": [
{
"token": []
}
],
"description": "登录日志删除",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"我的登录日志"
],
"summary": "登录日志删除",
"parameters": [
{
"description": "登录日志信息",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/model.LoginLog"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/my/login_log/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"
},
{
"type": "integer",
"description": "用户ID",
"name": "user_id",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/model.LoginLogList"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/my/peer/list": { "/admin/my/peer/list": {
"get": { "get": {
"security": [ "security": [
@@ -3884,40 +4051,6 @@ const docTemplateadmin = `{
} }
} }
}, },
"/admin/server-config": {
"get": {
"security": [
{
"token": []
}
],
"description": "服务配置,给webclient提供api-server",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"ADMIN"
],
"summary": "RUSTDESK服务配置",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/share_record/batchDelete": { "/admin/share_record/batchDelete": {
"post": { "post": {
"security": [ "security": [
@@ -5768,6 +5901,9 @@ const docTemplateadmin = `{
"ip": { "ip": {
"type": "string" "type": "string"
}, },
"is_deleted": {
"type": "integer"
},
"platform": { "platform": {
"description": "windows,linux,mac,android,ios", "description": "windows,linux,mac,android,ios",
"type": "string" "type": "string"

View File

@@ -974,40 +974,6 @@
} }
} }
}, },
"/admin/app-config": {
"get": {
"security": [
{
"token": []
}
],
"description": "APP服务配置",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"ADMIN"
],
"summary": "APP服务配置",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/audit_conn/batchDelete": { "/admin/audit_conn/batchDelete": {
"post": { "post": {
"security": [ "security": [
@@ -1874,7 +1840,7 @@
} }
} }
}, },
"/admin/login_log/delete": { "/admin/login_log/batchDelete": {
"post": { "post": {
"security": [ "security": [
{ {
@@ -1919,6 +1885,51 @@
} }
} }
}, },
"/admin/login_log/delete": {
"post": {
"security": [
{
"token": []
}
],
"description": "登录日志删除",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"登录日志"
],
"summary": "登录日志删除",
"parameters": [
{
"description": "登录日志信息",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/model.LoginLog"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/login_log/detail/{id}": { "/admin/login_log/detail/{id}": {
"get": { "get": {
"security": [ "security": [
@@ -2750,6 +2761,162 @@
} }
} }
}, },
"/admin/my/login_log/batchDelete": {
"post": {
"security": [
{
"token": []
}
],
"description": "登录日志批量删除",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"我的登录日志"
],
"summary": "登录日志批量删除",
"parameters": [
{
"description": "登录日志",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/admin.LoginLogIds"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/my/login_log/delete": {
"post": {
"security": [
{
"token": []
}
],
"description": "登录日志删除",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"我的登录日志"
],
"summary": "登录日志删除",
"parameters": [
{
"description": "登录日志信息",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/model.LoginLog"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/my/login_log/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"
},
{
"type": "integer",
"description": "用户ID",
"name": "user_id",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/model.LoginLogList"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/my/peer/list": { "/admin/my/peer/list": {
"get": { "get": {
"security": [ "security": [
@@ -3877,40 +4044,6 @@
} }
} }
}, },
"/admin/server-config": {
"get": {
"security": [
{
"token": []
}
],
"description": "服务配置,给webclient提供api-server",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"ADMIN"
],
"summary": "RUSTDESK服务配置",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/share_record/batchDelete": { "/admin/share_record/batchDelete": {
"post": { "post": {
"security": [ "security": [
@@ -5761,6 +5894,9 @@
"ip": { "ip": {
"type": "string" "type": "string"
}, },
"is_deleted": {
"type": "integer"
},
"platform": { "platform": {
"description": "windows,linux,mac,android,ios", "description": "windows,linux,mac,android,ios",
"type": "string" "type": "string"

View File

@@ -531,6 +531,8 @@ definitions:
type: integer type: integer
ip: ip:
type: string type: string
is_deleted:
type: integer
platform: platform:
description: windows,linux,mac,android,ios description: windows,linux,mac,android,ios
type: string type: string
@@ -1347,27 +1349,6 @@ paths:
summary: 地址簿规则编辑 summary: 地址簿规则编辑
tags: tags:
- 地址簿规则 - 地址簿规则
/admin/app-config:
get:
consumes:
- application/json
description: APP服务配置
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: APP服务配置
tags:
- ADMIN
/admin/audit_conn/batchDelete: /admin/audit_conn/batchDelete:
post: post:
consumes: consumes:
@@ -1892,7 +1873,7 @@ paths:
summary: 登录选项 summary: 登录选项
tags: tags:
- 登录 - 登录
/admin/login_log/delete: /admin/login_log/batchDelete:
post: post:
consumes: consumes:
- application/json - application/json
@@ -1920,6 +1901,34 @@ paths:
summary: 登录日志批量删除 summary: 登录日志批量删除
tags: tags:
- 登录日志 - 登录日志
/admin/login_log/delete:
post:
consumes:
- application/json
description: 登录日志删除
parameters:
- description: 登录日志信息
in: body
name: body
required: true
schema:
$ref: '#/definitions/model.LoginLog'
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/login_log/detail/{id}: /admin/login_log/detail/{id}:
get: get:
consumes: consumes:
@@ -2413,6 +2422,101 @@ paths:
summary: 地址簿规则编辑 summary: 地址簿规则编辑
tags: tags:
- 我的地址簿规则 - 我的地址簿规则
/admin/my/login_log/batchDelete:
post:
consumes:
- application/json
description: 登录日志批量删除
parameters:
- description: 登录日志
in: body
name: body
required: true
schema:
$ref: '#/definitions/admin.LoginLogIds'
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/my/login_log/delete:
post:
consumes:
- application/json
description: 登录日志删除
parameters:
- description: 登录日志信息
in: body
name: body
required: true
schema:
$ref: '#/definitions/model.LoginLog'
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/my/login_log/list:
get:
consumes:
- application/json
description: 登录日志列表
parameters:
- description: 页码
in: query
name: page
type: integer
- description: 页大小
in: query
name: page_size
type: integer
- description: 用户ID
in: query
name: user_id
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.Response'
- properties:
data:
$ref: '#/definitions/model.LoginLogList'
type: object
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
security:
- token: []
summary: 登录日志列表
tags:
- 我的登录日志
/admin/my/peer/list: /admin/my/peer/list:
get: get:
consumes: consumes:
@@ -3088,27 +3192,6 @@ paths:
summary: 设备编辑 summary: 设备编辑
tags: tags:
- 设备 - 设备
/admin/server-config:
get:
consumes:
- application/json
description: 服务配置,给webclient提供api-server
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: RUSTDESK服务配置
tags:
- ADMIN
/admin/share_record/batchDelete: /admin/share_record/batchDelete:
post: post:
consumes: consumes:

View File

@@ -653,40 +653,6 @@ const docTemplateapi = `{
} }
} }
}, },
"/api": {
"get": {
"security": [
{
"token": []
}
],
"description": "用户信息",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"用户"
],
"summary": "用户信息",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.UserPayload"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/audit/conn": { "/audit/conn": {
"post": { "post": {
"description": "审计连接", "description": "审计连接",
@@ -767,6 +733,40 @@ const docTemplateapi = `{
} }
} }
}, },
"/currentUser": {
"get": {
"security": [
{
"token": []
}
],
"description": "用户信息",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"用户"
],
"summary": "用户信息",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.UserPayload"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/heartbeat": { "/heartbeat": {
"post": { "post": {
"description": "心跳", "description": "心跳",
@@ -1178,43 +1178,6 @@ const docTemplateapi = `{
} }
} }
}, },
"/tags": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "标签",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"地址"
],
"summary": "标签",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/model.Tag"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/users": { "/users": {
"get": { "get": {
"security": [ "security": [
@@ -1289,6 +1252,35 @@ const docTemplateapi = `{
} }
} }
} }
},
"/version": {
"get": {
"description": "版本",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"首页"
],
"summary": "版本",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
} }
}, },
"definitions": { "definitions": {

View File

@@ -646,40 +646,6 @@
} }
} }
}, },
"/api": {
"get": {
"security": [
{
"token": []
}
],
"description": "用户信息",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"用户"
],
"summary": "用户信息",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.UserPayload"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/audit/conn": { "/audit/conn": {
"post": { "post": {
"description": "审计连接", "description": "审计连接",
@@ -760,6 +726,40 @@
} }
} }
}, },
"/currentUser": {
"get": {
"security": [
{
"token": []
}
],
"description": "用户信息",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"用户"
],
"summary": "用户信息",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.UserPayload"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/heartbeat": { "/heartbeat": {
"post": { "post": {
"description": "心跳", "description": "心跳",
@@ -1171,43 +1171,6 @@
} }
} }
}, },
"/tags": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "标签",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"地址"
],
"summary": "标签",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/model.Tag"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/users": { "/users": {
"get": { "get": {
"security": [ "security": [
@@ -1282,6 +1245,35 @@
} }
} }
} }
},
"/version": {
"get": {
"description": "版本",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"首页"
],
"summary": "版本",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
} }
}, },
"definitions": { "definitions": {

View File

@@ -598,27 +598,6 @@ paths:
summary: 标签 summary: 标签
tags: tags:
- 地址[Personal] - 地址[Personal]
/api:
get:
consumes:
- application/json
description: 用户信息
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.UserPayload'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
security:
- token: []
summary: 用户信息
tags:
- 用户
/audit/conn: /audit/conn:
post: post:
consumes: consumes:
@@ -671,6 +650,27 @@ paths:
summary: 审计文件 summary: 审计文件
tags: tags:
- 审计 - 审计
/currentUser:
get:
consumes:
- application/json
description: 用户信息
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.UserPayload'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
security:
- token: []
summary: 用户信息
tags:
- 用户
/heartbeat: /heartbeat:
post: post:
consumes: consumes:
@@ -936,29 +936,6 @@ paths:
summary: 提交系统信息 summary: 提交系统信息
tags: tags:
- 地址 - 地址
/tags:
post:
consumes:
- application/json
description: 标签
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/model.Tag'
type: array
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
security:
- BearerAuth: []
summary: 标签
tags:
- 地址
/users: /users:
get: get:
consumes: consumes:
@@ -1004,6 +981,25 @@ paths:
summary: 用户列表 summary: 用户列表
tags: tags:
- 群组 - 群组
/version:
get:
consumes:
- application/json
description: 版本
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
summary: 版本
tags:
- 首页
securityDefinitions: securityDefinitions:
BearerAuth: BearerAuth:
in: header in: header

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB

BIN
docs/init_admin_pwd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -4,16 +4,19 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/locales/en" "github.com/go-playground/locales/en"
"github.com/go-playground/locales/es" "github.com/go-playground/locales/es"
"github.com/go-playground/locales/fr"
"github.com/go-playground/locales/ko" "github.com/go-playground/locales/ko"
"github.com/go-playground/locales/ru" "github.com/go-playground/locales/ru"
"github.com/go-playground/locales/zh_Hans_CN" "github.com/go-playground/locales/zh_Hans_CN"
"github.com/go-playground/locales/zh_Hant"
ut "github.com/go-playground/universal-translator" ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
en_translations "github.com/go-playground/validator/v10/translations/en" en_translations "github.com/go-playground/validator/v10/translations/en"
es_translations "github.com/go-playground/validator/v10/translations/es" es_translations "github.com/go-playground/validator/v10/translations/es"
fr_translations "github.com/go-playground/validator/v10/translations/fr"
ru_translations "github.com/go-playground/validator/v10/translations/ru" ru_translations "github.com/go-playground/validator/v10/translations/ru"
zh_translations "github.com/go-playground/validator/v10/translations/zh" zh_translations "github.com/go-playground/validator/v10/translations/zh"
zh_tw_translations "github.com/go-playground/validator/v10/translations/zh_tw"
"reflect" "reflect"
) )
@@ -26,14 +29,18 @@ func ApiInitValidator() {
koT := ko.New() koT := ko.New()
ruT := ru.New() ruT := ru.New()
esT := es.New() esT := es.New()
frT := fr.New()
zhTwT := zh_Hant.New()
uni := ut.New(enT, cn, koT, ruT, esT) uni := ut.New(enT, cn, koT, ruT, esT, frT, zhTwT)
enTrans, _ := uni.GetTranslator("en") enTrans, _ := uni.GetTranslator("en")
zhTrans, _ := uni.GetTranslator("zh_Hans_CN") zhTrans, _ := uni.GetTranslator("zh_Hans_CN")
koTrans, _ := uni.GetTranslator("ko") koTrans, _ := uni.GetTranslator("ko")
ruTrans, _ := uni.GetTranslator("ru") ruTrans, _ := uni.GetTranslator("ru")
esTrans, _ := uni.GetTranslator("es") esTrans, _ := uni.GetTranslator("es")
frTrans, _ := uni.GetTranslator("fr")
zhTwTrans, _ := uni.GetTranslator("zh_Hant")
err := zh_translations.RegisterDefaultTranslations(validate, zhTrans) err := zh_translations.RegisterDefaultTranslations(validate, zhTrans)
if err != nil { if err != nil {
@@ -57,6 +64,14 @@ func ApiInitValidator() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
err = fr_translations.RegisterDefaultTranslations(validate, frTrans)
if err != nil {
panic(err)
}
err = zh_tw_translations.RegisterDefaultTranslations(validate, zhTwTrans)
if err != nil {
panic(err)
}
validate.RegisterTagNameFunc(func(field reflect.StructField) string { validate.RegisterTagNameFunc(func(field reflect.StructField) string {
label := field.Tag.Get("label") label := field.Tag.Get("label")
@@ -117,6 +132,13 @@ func getTranslatorForLang(lang string) ut.Translator {
case "zh": case "zh":
trans, _ := Validator.UT.GetTranslator("zh_Hans_CN") trans, _ := Validator.UT.GetTranslator("zh_Hans_CN")
return trans return trans
case "zh_TW":
fallthrough
case "zh-TW":
fallthrough
case "zh-tw":
trans, _ := Validator.UT.GetTranslator("zh_Hant")
return trans
case "ko": case "ko":
trans, _ := Validator.UT.GetTranslator("ko") trans, _ := Validator.UT.GetTranslator("ko")
return trans return trans
@@ -126,6 +148,9 @@ func getTranslatorForLang(lang string) ut.Translator {
case "es": case "es":
trans, _ := Validator.UT.GetTranslator("es") trans, _ := Validator.UT.GetTranslator("es")
return trans return trans
case "fr":
trans, _ := Validator.UT.GetTranslator("fr")
return trans
case "en": case "en":
fallthrough fallthrough
default: default:

13
go.mod
View File

@@ -13,7 +13,7 @@ require (
github.com/go-playground/validator/v10 v10.11.2 github.com/go-playground/validator/v10 v10.11.2
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.1.2 github.com/google/uuid v1.6.0
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
@@ -22,13 +22,14 @@ require (
github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.3 github.com/swaggo/swag v1.16.3
golang.org/x/oauth2 v0.23.0 golang.org/x/oauth2 v0.23.0
golang.org/x/text v0.18.0 golang.org/x/text v0.21.0
gorm.io/driver/mysql v1.5.7 gorm.io/driver/mysql v1.5.7
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.7
) )
require ( require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
@@ -37,6 +38,8 @@ require (
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // 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/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-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
@@ -70,10 +73,10 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect github.com/ugorji/go/codec v1.2.9 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.23.0 // indirect golang.org/x/crypto v0.31.0 // indirect
golang.org/x/image v0.13.0 // indirect golang.org/x/image v0.13.0 // indirect
golang.org/x/net v0.25.0 // indirect golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.25.0 // indirect golang.org/x/sys v0.28.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // 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

View File

@@ -182,15 +182,20 @@ func (ct *Login) Login(c *gin.Context) {
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "UsernameOrPasswordError", c.RemoteIP(), clientIp)) global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "UsernameOrPasswordError", c.RemoteIP(), clientIp))
loginLimiter.RecordFailure(clientIp) loginLimiter.RecordFailure(clientIp)
if loginLimiter.NeedsCaptcha(clientIp) { if loginLimiter.NeedsCaptcha(clientIp) {
// 移除原验证码,重新生成
loginLimiter.RemoveCaptcha(clientIp) loginLimiter.RemoveCaptcha(clientIp)
response.Fail(c, 110, response.TranslateMsg(c, "UsernameOrPasswordError"))
return
} }
response.Fail(c, 101, response.TranslateMsg(c, "UsernameOrPasswordError")) response.Fail(c, 101, response.TranslateMsg(c, "UsernameOrPasswordError"))
return return
} }
if !service.AllService.UserService.CheckUserEnable(u) {
if loginLimiter.NeedsCaptcha(clientIp) {
loginLimiter.RemoveCaptcha(clientIp)
}
response.Fail(c, 101, response.TranslateMsg(c, "UserDisabled"))
return
}
ut := service.AllService.UserService.Login(u, &model.LoginLog{ ut := service.AllService.UserService.Login(u, &model.LoginLog{
UserId: u.Id, UserId: u.Id,
Client: model.LoginLogClientWebAdmin, Client: model.LoginLogClientWebAdmin,

View File

@@ -56,10 +56,6 @@ func (ct *LoginLog) List(c *gin.Context) {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error()) response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
return return
} }
u := service.AllService.UserService.CurUser(c)
if !service.AllService.UserService.IsAdmin(u) || query.IsMy == 1 {
query.UserId = int(u.Id)
}
res := service.AllService.LoginLogService.List(query.Page, query.PageSize, func(tx *gorm.DB) { res := service.AllService.LoginLogService.List(query.Page, query.PageSize, func(tx *gorm.DB) {
if query.UserId > 0 { if query.UserId > 0 {
tx.Where("user_id = ?", query.UserId) tx.Where("user_id = ?", query.UserId)
@@ -93,21 +89,16 @@ func (ct *LoginLog) Delete(c *gin.Context) {
return return
} }
l := service.AllService.LoginLogService.InfoById(f.Id) l := service.AllService.LoginLogService.InfoById(f.Id)
u := service.AllService.UserService.CurUser(c) if l.Id == 0 {
if !service.AllService.UserService.IsAdmin(u) && l.UserId != u.Id { response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound"))
response.Fail(c, 101, response.TranslateMsg(c, "NoAccess"))
return return
} }
if l.Id > 0 { err := service.AllService.LoginLogService.Delete(l)
err := service.AllService.LoginLogService.Delete(l) if err == nil {
if err == nil { response.Success(c, nil)
response.Success(c, nil)
return
}
response.Fail(c, 101, err.Error())
return return
} }
response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound")) response.Fail(c, 101, err.Error())
} }
// BatchDelete 删除 // BatchDelete 删除
@@ -119,7 +110,7 @@ func (ct *LoginLog) Delete(c *gin.Context) {
// @Param body body admin.LoginLogIds true "登录日志" // @Param body body admin.LoginLogIds true "登录日志"
// @Success 200 {object} response.Response // @Success 200 {object} response.Response
// @Failure 500 {object} response.Response // @Failure 500 {object} response.Response
// @Router /admin/login_log/delete [post] // @Router /admin/login_log/batchDelete [post]
// @Security token // @Security token
func (ct *LoginLog) BatchDelete(c *gin.Context) { func (ct *LoginLog) BatchDelete(c *gin.Context) {
f := &admin.LoginLogIds{} f := &admin.LoginLogIds{}

View File

@@ -0,0 +1,113 @@
package my
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type LoginLog struct {
}
// List 列表
// @Tags 我的登录日志
// @Summary 登录日志列表
// @Description 登录日志列表
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param page_size query int false "页大小"
// @Param user_id query int false "用户ID"
// @Success 200 {object} response.Response{data=model.LoginLogList}
// @Failure 500 {object} response.Response
// @Router /admin/my/login_log/list [get]
// @Security token
func (ct *LoginLog) List(c *gin.Context) {
query := &admin.LoginLogQuery{}
if err := c.ShouldBindQuery(query); err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
return
}
u := service.AllService.UserService.CurUser(c)
res := service.AllService.LoginLogService.List(query.Page, query.PageSize, func(tx *gorm.DB) {
tx.Where("user_id = ? and is_deleted = ?", u.Id, model.IsDeletedNo)
tx.Order("id desc")
})
response.Success(c, res)
}
// Delete 删除
// @Tags 我的登录日志
// @Summary 登录日志删除
// @Description 登录日志删除
// @Accept json
// @Produce json
// @Param body body model.LoginLog true "登录日志信息"
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/my/login_log/delete [post]
// @Security token
func (ct *LoginLog) Delete(c *gin.Context) {
f := &model.LoginLog{}
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
}
l := service.AllService.LoginLogService.InfoById(f.Id)
if l.Id == 0 || l.IsDeleted == model.IsDeletedYes {
response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound"))
return
}
u := service.AllService.UserService.CurUser(c)
if l.UserId != u.Id {
response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound"))
return
}
err := service.AllService.LoginLogService.SoftDelete(l)
if err == nil {
response.Success(c, nil)
return
}
response.Fail(c, 101, err.Error())
}
// BatchDelete 删除
// @Tags 我的登录日志
// @Summary 登录日志批量删除
// @Description 登录日志批量删除
// @Accept json
// @Produce json
// @Param body body admin.LoginLogIds true "登录日志"
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/my/login_log/batchDelete [post]
// @Security token
func (ct *LoginLog) BatchDelete(c *gin.Context) {
f := &admin.LoginLogIds{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
return
}
if len(f.Ids) == 0 {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError"))
return
}
u := service.AllService.UserService.CurUser(c)
err := service.AllService.LoginLogService.BatchSoftDelete(u.Id, f.Ids)
if err == nil {
response.Success(c, nil)
return
}
response.Fail(c, 101, err.Error())
return
}

View File

@@ -2,45 +2,127 @@ package admin
import ( import (
"Gwen/global" "Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response" "Gwen/http/response"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type Rustdesk struct { type Rustdesk struct {
} }
// ServerConfig RUSTDESK服务配置 type RustdeskCmd struct {
// @Tags ADMIN Cmd string `json:"cmd"`
// @Summary RUSTDESK服务配置 Option string `json:"option"`
// @Description 服务配置,给webclient提供api-server Target string `json:"target"`
// @Accept json
// @Produce json
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/server-config [get]
// @Security token
func (r *Rustdesk) ServerConfig(c *gin.Context) {
cf := &response.ServerConfigResponse{
IdServer: global.Config.Rustdesk.IdServer,
Key: global.Config.Rustdesk.Key,
RelayServer: global.Config.Rustdesk.RelayServer,
ApiServer: global.Config.Rustdesk.ApiServer,
}
response.Success(c, cf)
} }
// AppConfig APP服务配置 func (r *Rustdesk) CmdList(c *gin.Context) {
// @Tags ADMIN q := &admin.PageQuery{}
// @Summary APP服务配置 if err := c.ShouldBindQuery(q); err != nil {
// @Description APP服务配置 response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
// @Accept json return
// @Produce json }
// @Success 200 {object} response.Response res := service.AllService.ServerCmdService.List(q.Page, 9999)
// @Failure 500 {object} response.Response //在列表前添加系统命令
// @Router /admin/app-config [get] list := make([]*model.ServerCmd, 0)
// @Security token list = append(list, model.SysIdServerCmds...)
func (r *Rustdesk) AppConfig(c *gin.Context) { list = append(list, model.SysRelayServerCmds...)
response.Success(c, &gin.H{ list = append(list, res.ServerCmds...)
"web_client": global.Config.App.WebClient, res.ServerCmds = list
}) response.Success(c, res)
}
func (r *Rustdesk) CmdDelete(c *gin.Context) {
f := &model.ServerCmd{}
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
}
ex := service.AllService.ServerCmdService.Info(f.Id)
if ex.Id == 0 {
response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound"))
return
}
err := service.AllService.ServerCmdService.Delete(ex)
if err != nil {
response.Fail(c, 101, err.Error())
return
}
response.Success(c, nil)
}
func (r *Rustdesk) CmdCreate(c *gin.Context) {
f := &model.ServerCmd{}
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
}
err := service.AllService.ServerCmdService.Create(f)
if err != nil {
response.Fail(c, 101, err.Error())
return
}
response.Success(c, nil)
}
func (r *Rustdesk) CmdUpdate(c *gin.Context) {
f := &model.ServerCmd{}
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
}
ex := service.AllService.ServerCmdService.Info(f.Id)
if ex.Id == 0 {
response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound"))
return
}
err := service.AllService.ServerCmdService.Update(f)
if err != nil {
response.Fail(c, 101, err.Error())
return
}
response.Success(c, nil)
}
func (r *Rustdesk) SendCmd(c *gin.Context) {
rc := &RustdeskCmd{}
if err := c.ShouldBindJSON(rc); err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
return
}
if rc.Cmd == "" {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError"))
return
}
if rc.Target == "" {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError"))
return
}
if rc.Target != model.ServerCmdTargetIdServer && rc.Target != model.ServerCmdTargetRelayServer {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError"))
return
}
res, err := service.AllService.ServerCmdService.SendCmd(rc.Target, rc.Cmd, rc.Option)
if err != nil {
response.Fail(c, 101, err.Error())
return
}
response.Success(c, res)
} }

View File

@@ -32,8 +32,8 @@ type Ab struct {
func (a *Ab) Ab(c *gin.Context) { func (a *Ab) Ab(c *gin.Context) {
user := service.AllService.UserService.CurUser(c) user := service.AllService.UserService.CurUser(c)
al := service.AllService.AddressBookService.ListByUserId(user.Id, 1, 1000) al := service.AllService.AddressBookService.ListByUserIdAndCollectionId(user.Id, 0, 1, 1000)
tags := service.AllService.TagService.ListByUserId(user.Id) tags := service.AllService.TagService.ListByUserIdAndCollectionId(user.Id, 0)
tagColors := map[string]uint{} tagColors := map[string]uint{}
//将tags中的name转成一个以逗号分割的字符串 //将tags中的name转成一个以逗号分割的字符串
@@ -98,23 +98,6 @@ func (a *Ab) UpAb(c *gin.Context) {
c.JSON(http.StatusOK, nil) c.JSON(http.StatusOK, nil)
} }
// Tags
// @Tags 地址
// @Summary 标签
// @Description 标签
// @Accept json
// @Produce json
// @Success 200 {object} []model.Tag
// @Failure 500 {object} response.ErrorResponse
// @Router /tags [post]
// @Security BearerAuth
func (a *Ab) Tags(c *gin.Context) {
user := service.AllService.UserService.CurUser(c)
tags := service.AllService.TagService.ListByUserId(user.Id)
c.JSON(http.StatusOK, tags.Tags)
}
// PTags // PTags
// @Tags 地址[Personal] // @Tags 地址[Personal]
// @Summary 标签 // @Summary 标签

View File

@@ -7,6 +7,7 @@ import (
"Gwen/service" "Gwen/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http" "net/http"
"os"
"time" "time"
) )
@@ -61,3 +62,25 @@ func (i *Index) Heartbeat(c *gin.Context) {
} }
c.JSON(http.StatusOK, gin.H{}) c.JSON(http.StatusOK, gin.H{})
} }
// Version 版本
// @Tags 首页
// @Summary 版本
// @Description 版本
// @Accept json
// @Produce json
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @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
}
response.Success(
c,
string(v),
)
}

View File

@@ -51,6 +51,11 @@ func (l *Login) Login(c *gin.Context) {
return return
} }
if !service.AllService.UserService.CheckUserEnable(u) {
response.Error(c, response.TranslateMsg(c, "UserDisabled"))
return
}
//根据refer判断是webclient还是app //根据refer判断是webclient还是app
ref := c.GetHeader("referer") ref := c.GetHeader("referer")
if ref != "" { if ref != "" {
@@ -85,7 +90,9 @@ func (l *Login) Login(c *gin.Context) {
// @Router /login-options [get] // @Router /login-options [get]
func (l *Login) LoginOptions(c *gin.Context) { func (l *Login) LoginOptions(c *gin.Context) {
ops := service.AllService.OauthService.GetOauthProviders() ops := service.AllService.OauthService.GetOauthProviders()
ops = append(ops, model.OauthTypeWebauth) if global.Config.App.WebSso {
ops = append(ops, model.OauthTypeWebauth)
}
var oidcItems []map[string]string var oidcItems []map[string]string
for _, v := range ops { for _, v := range ops {
oidcItems = append(oidcItems, map[string]string{"name": v}) oidcItems = append(oidcItems, map[string]string{"name": v})

View File

@@ -34,7 +34,7 @@ type User struct {
// @Produce json // @Produce json
// @Success 200 {object} apiResp.UserPayload // @Success 200 {object} apiResp.UserPayload
// @Failure 500 {object} response.Response // @Failure 500 {object} response.Response
// @Router /api [get] // @Router /currentUser [get]
// @Security token // @Security token
func (u *User) Info(c *gin.Context) { func (u *User) Info(c *gin.Context) {
user := service.AllService.UserService.CurUser(c) user := service.AllService.UserService.CurUser(c)

View File

@@ -26,7 +26,7 @@ func (i *WebClient) ServerConfig(c *gin.Context) {
u := service.AllService.UserService.CurUser(c) u := service.AllService.UserService.CurUser(c)
peers := map[string]*api.WebClientPeerPayload{} peers := map[string]*api.WebClientPeerPayload{}
abs := service.AllService.AddressBookService.ListByUserId(u.Id, 1, 100) abs := service.AllService.AddressBookService.ListByUserIdAndCollectionId(u.Id, 0, 1, 100)
for _, ab := range abs.AddressBooks { for _, ab := range abs.AddressBooks {
pp := &api.WebClientPeerPayload{} pp := &api.WebClientPeerPayload{}
pp.FromAddressBook(ab) pp.FromAddressBook(ab)
@@ -64,12 +64,15 @@ func (i *WebClient) SharedPeer(c *gin.Context) {
response.Fail(c, 101, "share not found") response.Fail(c, 101, "share not found")
return return
} }
//判断是否过期,created_at + expire > now if sr.Expire != 0 {
ca := time.Time(sr.CreatedAt) //判断是否过期,created_at + expire > now
if ca.Add(time.Second * time.Duration(sr.Expire)).Before(time.Now()) { ca := time.Time(sr.CreatedAt)
response.Fail(c, 101, "share expired") if ca.Add(time.Second * time.Duration(sr.Expire)).Before(time.Now()) {
return response.Fail(c, 101, "share expired")
return
}
} }
ab := service.AllService.AddressBookService.InfoByUserIdAndId(sr.UserId, sr.PeerId) ab := service.AllService.AddressBookService.InfoByUserIdAndId(sr.UserId, sr.PeerId)
if ab.RowId == 0 { if ab.RowId == 0 {
response.Fail(c, 101, "peer not found") response.Fail(c, 101, "peer not found")

View File

@@ -3,6 +3,7 @@ package web
import ( import (
"Gwen/global" "Gwen/global"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"strconv"
) )
type Index struct { type Index struct {
@@ -14,11 +15,13 @@ func (i *Index) Index(c *gin.Context) {
func (i *Index) ConfigJs(c *gin.Context) { func (i *Index) ConfigJs(c *gin.Context) {
apiServer := global.Config.Rustdesk.ApiServer apiServer := global.Config.Rustdesk.ApiServer
magicQueryonline := strconv.Itoa(global.Config.Rustdesk.WebclientMagicQueryonline)
tmp := ` tmp := `
localStorage.setItem('api-server', "` + apiServer + `") localStorage.setItem('api-server', "` + apiServer + `")
const ws2_prefix = 'wc-' const ws2_prefix = 'wc-'
localStorage.setItem(ws2_prefix+'api-server', "` + apiServer + `") localStorage.setItem(ws2_prefix+'api-server', "` + apiServer + `")
`
window.webclient_magic_queryonline = ` + magicQueryonline + ``
c.String(200, tmp) c.String(200, tmp)
} }

View File

@@ -24,6 +24,14 @@ func AdminAuth() gin.HandlerFunc {
return return
} }
if !service.AllService.UserService.CheckUserEnable(user) {
c.JSON(401, gin.H{
"error": "Unauthorized",
})
c.Abort()
return
}
c.Set("curUser", user) c.Set("curUser", user)
c.Set("token", token) c.Set("token", token)
//如果时间小于1天,token自动续期 //如果时间小于1天,token自动续期

View File

@@ -1,6 +1,7 @@
package middleware package middleware
import ( import (
"Gwen/global"
"Gwen/service" "Gwen/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -27,7 +28,21 @@ func RustAuth() gin.HandlerFunc {
//提取token格式是Bearer {token} //提取token格式是Bearer {token}
//这里只是简单的提取 //这里只是简单的提取
token = token[7:] token = token[7:]
//验证token //验证token
//检查是否设置了jwt key
if len(global.Jwt.Key) > 0 {
uid, _ := service.AllService.UserService.VerifyJWT(token)
if uid == 0 {
c.JSON(401, gin.H{
"error": "Unauthorized",
})
c.Abort()
return
}
}
user, ut := service.AllService.UserService.InfoByAccessToken(token) user, ut := service.AllService.UserService.InfoByAccessToken(token)
if user.Id == 0 { if user.Id == 0 {
c.JSON(401, gin.H{ c.JSON(401, gin.H{
@@ -38,7 +53,7 @@ func RustAuth() gin.HandlerFunc {
} }
if !service.AllService.UserService.CheckUserEnable(user) { if !service.AllService.UserService.CheckUserEnable(user) {
c.JSON(401, gin.H{ c.JSON(401, gin.H{
"error": "账号已被禁用", "error": "Unauthorized",
}) })
c.Abort() c.Abort()
return return

View File

@@ -18,11 +18,6 @@ func (lp *LoginPayload) FromUser(user *model.User) {
lp.Nickname = user.Nickname lp.Nickname = user.Nickname
} }
var UserRouteNames = []string{
"MyTagList", "MyAddressBookList", "MyInfo", "MyAddressBookCollection", "MyPeer", "MyShareRecordList",
}
var AdminRouteNames = []string{"*"}
type UserOauthItem struct { type UserOauthItem struct {
Op string `json:"op"` Op string `json:"op"`
Status int `json:"status"` Status int `json:"status"`

View File

@@ -46,9 +46,20 @@ func Init(g *gin.Engine) {
ShareRecordBind(adg) ShareRecordBind(adg)
MyBind(adg) MyBind(adg)
RustdeskCmdBind(adg)
//访问静态文件 //访问静态文件
//g.StaticFS("/upload", http.Dir(global.Config.Gin.ResourcesPath+"/upload")) //g.StaticFS("/upload", http.Dir(global.Config.Gin.ResourcesPath+"/upload"))
} }
func RustdeskCmdBind(adg *gin.RouterGroup) {
cont := &admin.Rustdesk{}
rg := adg.Group("/rustdesk")
rg.POST("/sendCmd", cont.SendCmd)
rg.GET("/cmdList", cont.CmdList)
rg.POST("/cmdDelete", cont.CmdDelete)
rg.POST("/cmdCreate", cont.CmdCreate)
}
func LoginBind(rg *gin.RouterGroup) { func LoginBind(rg *gin.RouterGroup) {
cont := &admin.Login{} cont := &admin.Login{}
rg.POST("/login", cont.Login) rg.POST("/login", cont.Login)
@@ -160,8 +171,8 @@ func OauthBind(rg *gin.RouterGroup) {
} }
func LoginLogBind(rg *gin.RouterGroup) { func LoginLogBind(rg *gin.RouterGroup) {
aR := rg.Group("/login_log")
cont := &admin.LoginLog{} cont := &admin.LoginLog{}
aR := rg.Group("/login_log").Use(middleware.AdminPrivilege())
aR.GET("/list", cont.List) aR.GET("/list", cont.List)
aR.POST("/delete", cont.Delete) aR.POST("/delete", cont.Delete)
aR.POST("/batchDelete", cont.BatchDelete) aR.POST("/batchDelete", cont.BatchDelete)
@@ -274,6 +285,13 @@ func MyBind(rg *gin.RouterGroup) {
rg.GET("/my/peer/list", cont.List) rg.GET("/my/peer/list", cont.List)
} }
{
cont := &my.LoginLog{}
rg.GET("/my/login_log/list", cont.List)
rg.POST("/my/login_log/delete", cont.Delete)
rg.POST("/my/login_log/batchDelete", cont.BatchDelete)
}
} }
func ShareRecordBind(rg *gin.RouterGroup) { func ShareRecordBind(rg *gin.RouterGroup) {

View File

@@ -21,10 +21,13 @@ func ApiInit(g *gin.Engine) {
frg := g.Group("/api") frg := g.Group("/api")
i := &api.Index{} {
frg.GET("/", i.Index) i := &api.Index{}
frg.GET("/", i.Index)
frg.GET("/version", i.Version)
frg.POST("/heartbeat", i.Heartbeat) frg.POST("/heartbeat", i.Heartbeat)
}
{ {
l := &api.Login{} l := &api.Login{}
@@ -33,6 +36,7 @@ func ApiInit(g *gin.Engine) {
frg.POST("/login", l.Login) frg.POST("/login", l.Login)
} }
{ {
o := &api.Oauth{} o := &api.Oauth{}
// [method:POST] [uri:/api/oidc/auth] // [method:POST] [uri:/api/oidc/auth]
@@ -52,11 +56,15 @@ func ApiInit(g *gin.Engine) {
if global.Config.App.WebClient == 1 { if global.Config.App.WebClient == 1 {
WebClientRoutes(frg) WebClientRoutes(frg)
} }
au := &api.Audit{}
//[method:POST] [uri:/api/audit/conn] {
frg.POST("/audit/conn", au.AuditConn) au := &api.Audit{}
//[method:POST] [uri:/api/audit/file] //[method:POST] [uri:/api/audit/conn]
frg.POST("/audit/file", au.AuditFile) frg.POST("/audit/conn", au.AuditConn)
//[method:POST] [uri:/api/audit/file]
frg.POST("/audit/file", au.AuditFile)
}
frg.Use(middleware.RustAuth()) frg.Use(middleware.RustAuth())
{ {
u := &api.User{} u := &api.User{}

View File

@@ -1,14 +1,13 @@
package jwt package jwt
import ( import (
"crypto/rsa" "fmt"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"os"
"time" "time"
) )
type Jwt struct { type Jwt struct {
privateKey *rsa.PrivateKey Key []byte
TokenExpireDuration time.Duration TokenExpireDuration time.Duration
} }
@@ -17,31 +16,28 @@ type UserClaims struct {
jwt.RegisteredClaims jwt.RegisteredClaims
} }
func NewJwt(privateKeyFile string, tokenExpireDuration time.Duration) *Jwt { func NewJwt(key string, tokenExpireDuration time.Duration) *Jwt {
privateKeyContent, err := os.ReadFile(privateKeyFile)
if err != nil {
panic(err)
}
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyContent)
if err != nil {
panic(err)
}
return &Jwt{ return &Jwt{
privateKey: privateKey, Key: []byte(key),
TokenExpireDuration: tokenExpireDuration, TokenExpireDuration: tokenExpireDuration,
} }
} }
func (s *Jwt) GenerateToken(userId uint) string { func (s *Jwt) GenerateToken(userId uint) string {
t := jwt.NewWithClaims(jwt.SigningMethodRS256, if len(s.Key) == 0 {
fmt.Println("jwt key is nil")
return ""
}
t := jwt.NewWithClaims(jwt.SigningMethodHS256,
UserClaims{ UserClaims{
UserId: userId, UserId: userId,
RegisteredClaims: jwt.RegisteredClaims{ RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.TokenExpireDuration)), ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.TokenExpireDuration)),
}, },
}) })
token, err := t.SignedString(s.privateKey) token, err := t.SignedString(s.Key)
if err != nil { if err != nil {
fmt.Printf("jwt token generate error: %v", err)
return "" return ""
} }
return token return token
@@ -49,7 +45,7 @@ func (s *Jwt) GenerateToken(userId uint) string {
func (s *Jwt) ParseToken(tokenString string) (uint, error) { func (s *Jwt) ParseToken(tokenString string) (uint, error) {
token, err := jwt.ParseWithClaims(tokenString, &UserClaims{}, func(token *jwt.Token) (interface{}, error) { token, err := jwt.ParseWithClaims(tokenString, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
return s.privateKey.Public(), nil return s.Key, nil
}) })
if err != nil { if err != nil {
return 0, err return 0, err

View File

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

View File

@@ -4,12 +4,13 @@ type LoginLog struct {
IdModel IdModel
UserId uint `json:"user_id" gorm:"default:0;not null;"` UserId uint `json:"user_id" gorm:"default:0;not null;"`
Client string `json:"client"` //webadmin,webclient,app, Client string `json:"client"` //webadmin,webclient,app,
DeviceId string `json:"device_id"` DeviceId string `json:"device_id"`
Uuid string `json:"uuid"` Uuid string `json:"uuid"`
Ip string `json:"ip"` Ip string `json:"ip"`
Type string `json:"type"` //account,oauth Type string `json:"type"` //account,oauth
Platform string `json:"platform"` //windows,linux,mac,android,ios Platform string `json:"platform"` //windows,linux,mac,android,ios
UserTokenId uint `json:"user_token_id" gorm:"default:0;not null;"` UserTokenId uint `json:"user_token_id" gorm:"default:0;not null;"`
IsDeleted uint `json:"is_deleted" gorm:"default:0;not null;"`
TimeModel TimeModel
} }
@@ -24,6 +25,11 @@ const (
LoginLogTypeOauth = "oauth" LoginLogTypeOauth = "oauth"
) )
const (
IsDeletedNo = 0
IsDeletedYes = 1
)
type LoginLogList struct { type LoginLogList struct {
LoginLogs []*LoginLog `json:"list"` LoginLogs []*LoginLog `json:"list"`
Pagination Pagination

61
model/serverCmd.go Normal file
View File

@@ -0,0 +1,61 @@
package model
type ServerCmd struct {
IdModel
Cmd string `json:"cmd" gorm:"default:'';not null;"`
Alias string `json:"alias" gorm:"default:'';not null;"`
Option string `json:"option" gorm:"default:'';not null;"`
Explain string `json:"explain" gorm:"default:'';not null;"`
Target string `json:"target" gorm:"default:'';not null;"`
TimeModel
}
type ServerCmdList struct {
ServerCmds []*ServerCmd `json:"list"`
Pagination
}
const (
ServerCmdTargetIdServer = "21115"
ServerCmdTargetRelayServer = "21117"
)
var SysIdServerCmds = []*ServerCmd{
{Cmd: "h", Option: "", Explain: "show help", Target: ServerCmdTargetIdServer},
{Cmd: "relay-servers", Alias: "rs", Option: "<separated by ,>", Explain: "set or show relay servers", Target: ServerCmdTargetIdServer},
{Cmd: "ip-blocker", Alias: "ib", Option: "[<ip>|<number>] [-]", Explain: "block or unblock ip or show blocked ip", Target: ServerCmdTargetIdServer},
{Cmd: "ip-changes", Alias: "ic", Option: "[<id>|<number>] [-]", Explain: "ip-changes(ic) [<id>|<number>] [-]", Target: ServerCmdTargetIdServer},
{Cmd: "always-use-relay", Alias: "aur", Option: "[y|n]", Explain: "always use relay", Target: ServerCmdTargetIdServer},
{Cmd: "test-geo", Alias: "tg", Option: "<ip1> <ip2>", Explain: "test geo", Target: ServerCmdTargetIdServer},
}
/*
"blacklist-add(ba) <ip>",
"blacklist-remove(br) <ip>",
"blacklist(b) <ip>",
"blocklist-add(Ba) <ip>",
"blocklist-remove(Br) <ip>",
"blocklist(B) <ip>",
"downgrade-threshold(dt) [value]",
"downgrade-start-check(t) [value(second)]",
"limit-speed(ls) [value(Mb/s)]",
"total-bandwidth(tb) [value(Mb/s)]",
"single-bandwidth(sb) [value(Mb/s)]",
"usage(u)"
*/
var SysRelayServerCmds = []*ServerCmd{
{Cmd: "h", Option: "", Explain: "show help", Target: ServerCmdTargetRelayServer},
{Cmd: "blacklist-add", Alias: "ba", Option: "<ip>", Explain: "blacklist-add(ba) <ip>", Target: ServerCmdTargetRelayServer},
{Cmd: "blacklist-remove", Alias: "br", Option: "<ip>", Explain: "blacklist-remove(br) <ip>", Target: ServerCmdTargetRelayServer},
{Cmd: "blacklist", Alias: "b", Option: "<ip>", Explain: "blacklist(b) <ip>", Target: ServerCmdTargetRelayServer},
{Cmd: "blocklist-add", Alias: "Ba", Option: "<ip>", Explain: "blocklist-add(Ba) <ip>", Target: ServerCmdTargetRelayServer},
{Cmd: "blocklist-remove", Alias: "Br", Option: "<ip>", Explain: "blocklist-remove(Br) <ip>", Target: ServerCmdTargetRelayServer},
{Cmd: "blocklist", Alias: "B", Option: "<ip>", Explain: "blocklist(B) <ip>", Target: ServerCmdTargetRelayServer},
{Cmd: "downgrade-threshold", Alias: "dt", Option: "[value]", Explain: "downgrade-threshold(dt) [value]", Target: ServerCmdTargetRelayServer},
{Cmd: "downgrade-start-check", Alias: "t", Option: "[value(second)]", Explain: "downgrade-start-check(t) [value(second)]", Target: ServerCmdTargetRelayServer},
{Cmd: "limit-speed", Alias: "ls", Option: "[value(Mb/s)]", Explain: "limit-speed(ls) [value(Mb/s)]", Target: ServerCmdTargetRelayServer},
{Cmd: "total-bandwidth", Alias: "tb", Option: "[value(Mb/s)]", Explain: "total-bandwidth(tb) [value(Mb/s)]", Target: ServerCmdTargetRelayServer},
{Cmd: "single-bandwidth", Alias: "sb", Option: "[value(Mb/s)]", Explain: "single-bandwidth(sb) [value(Mb/s)]", Target: ServerCmdTargetRelayServer},
{Cmd: "usage", Alias: "u", Option: "", Explain: "usage(u)", Target: ServerCmdTargetRelayServer},
}

View File

@@ -27,3 +27,8 @@ type UserList struct {
Users []*User `json:"list,omitempty"` Users []*User `json:"list,omitempty"`
Pagination Pagination
} }
var UserRouteNames = []string{
"MyTagList", "MyAddressBookList", "MyInfo", "MyAddressBookCollection", "MyPeer", "MyShareRecordList", "MyLoginLog",
}
var AdminRouteNames = []string{"*"}

144
resources/i18n/fr.toml Normal file
View File

@@ -0,0 +1,144 @@
[Test]
description = "test"
one = "test1 "
other = "Test2 {{.P0}}"
[ParamsError]
description = "Params validation failed."
one = "La validation des paramètres a échoué."
other = "La validation des paramètres a échoué."
[OperationFailed]
description = "OperationFailed."
one = "l'opération a échoué."
other = "l'opération a échoué."
[OperationSuccess]
description = "OperationSuccess."
one = "Opération réussie"
other = "Opération réussie"
[ItemExists]
description = "Item already exists."
one = "L'élément existe déjà."
other = "L'élément existe déjà."
[ItemNotFound]
description = "Item not found."
one = "Article introuvable."
other = "Article introuvable."
[NoAccess]
description = "No access."
one = "Aucun d'access."
other = "Aucun d'access."
[UsernameOrPasswordError]
description = "Username or password error."
one = "Nom d'utilisateur ou de mot de passe incorrect."
other = "Nom d'utilisateur ou de mot de passe incorrect."
[SystemError]
description = "System error."
one = "Erreur system."
other = "Erreur system."
[ConfigNotFound]
description = "Config not found."
one = "Configuration introuvable."
other = "Configuration introuvable."
[OauthExpired]
description = "Oauth expired."
one = "Oauth a expiré, veuillez réessayer."
other = "Oauth a expiré, veuillez réessayer."
[OauthFailed]
description = "Oauth failed."
one = "Oauth a échoué."
other = "Oauth a échoué."
[OauthHasBindOtherUser]
description = "Oauth has bind other user."
one = "Oauth a lié un autre utilisateur."
other = "Oauth a lié un autre utilisateur."
[ParamIsEmpty]
description = "Param is empty."
one = "{{.P0}} est vide."
other = "{{.P0}} est vide."
[BindFail]
description = "Bind fail."
one = "Échec de la liaison."
other = "Échec de la liaison."
[BindSuccess]
description = "Bind success."
one = "Succès de la liaison."
other = "Succès de la liaison."
[OauthHasBeenSuccess]
description = "Oauth has been success."
one = "Oauth a été réussi avec Succès."
other = "Oauth a été réussi avec Succès."
[OauthSuccess]
description = "Oauth success."
one = "Oauh réussi avec succès."
other = "Oauh réussi avec succès."
[OauthRegisterSuccess]
description = "Oauth register success."
one = "Succès de l'enregistrement Oauth."
other = "Succès de l'enregistrement Oauth."
[OauthRegisterFailed]
description = "Oauth register failed."
one = "L'inscription Oauth a échoué."
other = "L'inscription Oauth a échoué."
[GetOauthTokenError]
description = "Get oauth token error."
one = "Erreur de l'obtention du jeton oauth."
other = "Erreur de l'obtention du jeton oauth."
[GetOauthUserInfoError]
description = "Get oauth user info error."
one = "Erreur d'obtention d'informations sur l'utilisateur oauth."
other = "Erreur d'obtention d'informations sur l'utilisateur oauth."
[DecodeOauthUserInfoError]
description = "Decode oauth user info error."
one = "Erreur de décodage des informations utilisateur oauth."
other = "Erreur de décodage des informations utilisateur oauth."
[OldPasswordError]
description = "Old password error."
one = "Ancien mot de passe incorrect."
other = "Ancien mot de passe incorrect."
[DefaultGroup]
description = "Default group"
one = "Groupe Défaut"
other = "Groupe Défaut"
[ShareGroup]
description = "Share group"
one = "Groupe partagé"
other = "Groupe partagé"
[RegisterClosed]
description = "Register closed."
one = "Inscription fermée."
other = "Inscription fermée."
[CaptchaRequired]
description = "Captcha required."
one = "Captcha requis."
other = "Captcha requis."
[CaptchaError]
description = "Captcha error."
one = "Erreur de captcha."
other = "Erreur de captcha."

137
resources/i18n/zh_TW.toml Normal file
View File

@@ -0,0 +1,137 @@
[Test]
description = "test"
one = "測試1 {{.P0}}"
other = "測試2 {{.P0}}"
[ParamsError]
description = "Params validation failed."
one = "引數錯誤。"
other = "引數錯誤。"
[OperationFailed]
description = "OperationFailed."
one = "操作失敗。"
other = "操作失敗。"
[OperationSuccess]
description = "OperationSuccess."
one = "操作成功。"
other = "操作成功。"
[ItemExists]
description = "Item already exists."
one = "資料已存在。"
other = "資料已存在。"
[ItemNotFound]
description = "Item not found."
one = "資料不存在。"
other = "資料不存在。"
[NoAccess]
description = "No access."
one = "無許可權。"
other = "無許可權。"
[UsernameOrPasswordError]
description = "Username or password error."
one = "使用者名稱或密碼錯誤。"
other = "使用者名稱或密碼錯誤。"
[SystemError]
description = "System error."
one = "系統錯誤。"
other = "系統錯誤。"
[ConfigNotFound]
description = "Config not found."
one = "配置不存在。"
other = "配置不存在。"
#授權過期
[OauthExpired]
description = "Oauth expired."
one = "授權過期,請重新授權。"
other = "授權過期,請重新授權。"
[OauthFailed]
description = "Oauth failed."
one = "授權失敗。"
other = "授權失敗。"
[OauthHasBindOtherUser]
description = "Oauth has bind other user."
one = "授權已繫結其他使用者。"
other = "授權已繫結其他使用者。"
[ParamIsEmpty]
description = "Param is empty."
one = "{{.P0}} 為空。"
other = "{{.P0}} 為空。"
[BindFail]
description = "Bind fail."
one = "繫結失敗。"
other = "繫結失敗。"
[BindSuccess]
description = "Bind success."
one = "繫結成功。"
other = "繫結成功。"
[OauthHasBeenSuccess]
description = "Oauth has been success."
one = "授權已成功。"
other = "授權已成功。"
[OauthSuccess]
description = "Oauth success."
one = "授權成功。"
other = "授權成功。"
[OauthRegisterSuccess]
description = "Oauth register success."
one = "授權註冊成功。"
other = "授權註冊成功。"
[OauthRegisterFailed]
description = "Oauth register failed."
one = "授權註冊失敗。"
other = "授權註冊失敗。"
[GetOauthTokenError]
description = "Get oauth token error."
one = "獲取授權token失敗。"
other = "獲取授權token失敗。"
[GetOauthUserInfoError]
description = "Get oauth user info error."
one = "獲取授權使用者資訊失敗。"
other = "獲取授權使用者資訊失敗。"
[DecodeOauthUserInfoError]
description = "Decode oauth user info error."
one = "解析授權使用者資訊失敗。"
other = "解析授權使用者資訊失敗。"
[OldPasswordError]
description = "Old password error."
one = "舊密碼錯誤。"
other = "舊密碼錯誤。"
[DefaultGroup]
description = "Default group."
one = "預設組"
other = "預設組"
[ShareGroup]
description = "Share group."
one = "共享組"
other = "共享組"
[RegisterClosed]
description = "Register closed."
one = "註冊已關閉。"
other = "註冊已關閉。"
[CaptchaRequired]
description = "Captcha required."
one = "需要驗證碼。"
other = "需要驗證碼。"
[CaptchaError]
description = "Captcha error."
one = "驗證碼錯誤。"
other = "驗證碼錯誤。"

1
resources/version Normal file
View File

@@ -0,0 +1 @@
v1.0.0

View File

@@ -32,7 +32,7 @@
<title>RustDesk</title> <title>RustDesk</title>
<script src="/webclient-config/index.js"></script> <script src="/webclient-config/index.js"></script>
<link rel="manifest" href="manifest.json" /> <link rel="manifest" href="manifest.json" />
<script type="module" crossorigin src="js/dist/index.js?v=893935a2"></script> <script type="module" crossorigin src="js/dist/index.js?v=cabfd933"></script>
<link rel="modulepreload" href="js/dist/vendor.js?v=0b990c6e" /> <link rel="modulepreload" href="js/dist/vendor.js?v=0b990c6e" />
<style> <style>
html, html,
@@ -259,7 +259,7 @@
} }
scriptLoaded = true; scriptLoaded = true;
var scriptTag = document.createElement("script"); var scriptTag = document.createElement("script");
scriptTag.src = "main.dart.js?v=df360f45"; scriptTag.src = "main.dart.js?v=060a626e";
scriptTag.type = "application/javascript"; scriptTag.type = "application/javascript";
document.body.append(scriptTag); document.body.append(scriptTag);
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

486
service/ldap.go Normal file
View File

@@ -0,0 +1,486 @@
package service
import (
"crypto/tls"
"errors"
"fmt"
"strconv"
"strings"
"github.com/go-ldap/ldap/v3"
"Gwen/config"
"Gwen/global"
"Gwen/model"
)
var (
ErrLdapNotEnabled = errors.New("LdapNotEnabled")
ErrLdapUserDisabled = errors.New("UserDisabledAtLdap")
ErrLdapUserNotFound = errors.New("UserNotFound")
ErrLdapMailNotMatch = errors.New("MailNotMatch")
ErrLdapConnectFailed = errors.New("LdapConnectFailed")
ErrLdapSearchFailed = errors.New("LdapSearchRequestFailed")
ErrLdapTlsFailed = errors.New("LdapStartTLSFailed")
ErrLdapBindService = errors.New("LdapBindServiceFailed")
ErrLdapBindFailed = errors.New("LdapBindFailed")
ErrLdapToLocalUserFailed = errors.New("LdapToLocalUserFailed")
ErrLdapCreateUserFailed = errors.New("LdapCreateUserFailed")
)
// LdapService is responsible for LDAP authentication and user synchronization.
type LdapService struct {
}
// LdapUser represents the user attributes retrieved from LDAP.
type LdapUser struct {
Dn string
Username string
Email string
FirstName string
LastName string
MemberOf []string
EnableAttrValue string
Enabled bool
}
// Name returns the full name of an LDAP user.
func (lu *LdapUser) Name() string {
return fmt.Sprintf("%s %s", lu.FirstName, lu.LastName)
}
// ToUser merges the LdapUser data into a provided *model.User.
// If 'u' is nil, it creates and returns a new *model.User.
func (lu *LdapUser) ToUser(u *model.User) *model.User {
if u == nil {
u = &model.User{}
}
u.Username = lu.Username
u.Email = lu.Email
u.Nickname = lu.Name()
if lu.Enabled {
u.Status = model.COMMON_STATUS_ENABLE
} else {
u.Status = model.COMMON_STATUS_DISABLED
}
return u
}
// 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)
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 {
conn.Close()
return nil, errors.Join(ErrLdapBindService, err)
}
return conn, nil
}
// connectAndBindAdmin creates an LDAP connection, optionally starts TLS, and then binds using the admin credentials.
func (ls *LdapService) connectAndBindAdmin(cfg *config.Ldap) (*ldap.Conn, error) {
return ls.connectAndBind(cfg, cfg.BindDn, cfg.BindPassword)
}
// verifyCredentials checks the provided username and password against LDAP.
func (ls *LdapService) verifyCredentials(cfg *config.Ldap, username, password string) error {
ldapConn, err := ls.connectAndBind(cfg, username, password)
if err != nil {
return err
}
defer ldapConn.Close()
return nil
}
// Authenticate checks the provided username and password against LDAP.
// Returns the corresponding *model.User if successful, or an error if not.
func (ls *LdapService) Authenticate(username, password string) (*model.User, error) {
ldapUser, err := ls.GetUserInfoByUsernameLdap(username)
if err != nil {
return nil, err
}
if !ldapUser.Enabled {
return nil, ErrLdapUserDisabled
}
cfg := &global.Config.Ldap
user, err := ls.mapToLocalUser(cfg, ldapUser)
if err != nil {
return nil, errors.Join(ErrLdapToLocalUserFailed, err)
}
return user, nil
}
// mapToLocalUser checks whether the user exists locally; if not, creates one.
// If the user exists and Ldap.Sync is enabled, it updates local info.
func (ls *LdapService) mapToLocalUser(cfg *config.Ldap, lu *LdapUser) (*model.User, error) {
userService := &UserService{}
localUser := userService.InfoByUsername(lu.Username)
isAdmin := ls.isUserAdmin(cfg, lu)
// If the user doesn't exist in local DB, create a new one
if localUser.Id == 0 {
newUser := lu.ToUser(nil)
// Typically, you dont store LDAP user passwords locally.
// If needed, you can set a random password here.
newUser.IsAdmin = &isAdmin
newUser.GroupId = 1
if err := global.DB.Create(newUser).Error; err != nil {
return nil, errors.Join(ErrLdapCreateUserFailed, err)
}
return userService.InfoByUsername(lu.Username), nil
}
// If the user already exists and sync is enabled, update local info
if cfg.User.Sync {
originalEmail := localUser.Email
originalNickname := localUser.Nickname
originalIsAdmin := localUser.IsAdmin
originalStatus := localUser.Status
lu.ToUser(localUser) // merges LDAP data into the existing user
localUser.IsAdmin = &isAdmin
if err := userService.Update(localUser); err != nil {
// If the update fails, revert to original data
localUser.Email = originalEmail
localUser.Nickname = originalNickname
localUser.IsAdmin = originalIsAdmin
localUser.Status = originalStatus
}
}
return localUser, nil
}
// 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
if !cfg.Enable {
return false
}
sr, err := ls.usernameSearchResult(cfg, username)
if err != nil {
return false
}
return len(sr.Entries) > 0
}
// 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
if !cfg.Enable {
return false
}
sr, err := ls.emailSearchResult(cfg, email)
if err != nil {
return false
}
return len(sr.Entries) > 0
}
// GetUserInfoByUsernameLdap returns the user info from LDAP for the given username.
func (ls *LdapService) GetUserInfoByUsernameLdap(username string) (*LdapUser, error) {
cfg := &global.Config.Ldap
if !cfg.Enable {
return nil, ErrLdapNotEnabled
}
sr, err := ls.usernameSearchResult(cfg, username)
if err != nil {
return nil, errors.Join(ErrLdapSearchFailed, err)
}
if len(sr.Entries) != 1 {
return nil, ErrLdapUserNotFound
}
return ls.userResultToLdapUser(cfg, sr.Entries[0]), nil
}
// GetUserInfoByUsernameLocal returns the user info from LDAP for the given username. If the user exists, it will sync the user info to the local database.
func (ls *LdapService) GetUserInfoByUsernameLocal(username string) (*model.User, error) {
ldapUser, err := ls.GetUserInfoByUsernameLdap(username)
if err != nil {
return &model.User{}, err
}
return ls.mapToLocalUser(&global.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
if !cfg.Enable {
return nil, ErrLdapNotEnabled
}
sr, err := ls.emailSearchResult(cfg, email)
if err != nil {
return nil, errors.Join(ErrLdapSearchFailed, err)
}
if len(sr.Entries) != 1 {
return nil, ErrLdapUserNotFound
}
return ls.userResultToLdapUser(cfg, sr.Entries[0]), nil
}
// GetUserInfoByEmailLocal returns the user info from LDAP for the given email. if the user exists, it will synchronize the user information to local database.
func (ls *LdapService) GetUserInfoByEmailLocal(email string) (*model.User, error) {
ldapUser, err := ls.GetUserInfoByEmailLdap(email)
if err != nil {
return &model.User{}, err
}
return ls.mapToLocalUser(&global.Config.Ldap, ldapUser)
}
// usernameSearchResult returns the search result for the given username.
func (ls *LdapService) usernameSearchResult(cfg *config.Ldap, username string) (*ldap.SearchResult, error) {
// Build the combined filter for the username
filter := ls.filterField(ls.fieldUsername(cfg), username)
// Create the *ldap.SearchRequest
searchRequest := ls.buildUserSearchRequest(cfg, filter)
return ls.searchResult(cfg, searchRequest)
}
// emailSearchResult returns the search result for the given email.
func (ls *LdapService) emailSearchResult(cfg *config.Ldap, email string) (*ldap.SearchResult, error) {
filter := ls.filterField(ls.fieldEmail(cfg), email)
searchRequest := ls.buildUserSearchRequest(cfg, filter)
return ls.searchResult(cfg, searchRequest)
}
func (ls *LdapService) searchResult(cfg *config.Ldap, searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) {
ldapConn, err := ls.connectAndBindAdmin(cfg)
if err != nil {
return nil, err
}
defer ldapConn.Close()
return ldapConn.Search(searchRequest)
}
// buildUserSearchRequest constructs an LDAP SearchRequest for users given a filter.
func (ls *LdapService) buildUserSearchRequest(cfg *config.Ldap, filter string) *ldap.SearchRequest {
baseDn := ls.baseDnUser(cfg) // user-specific base DN, or fallback
filterConfig := cfg.User.Filter
if filterConfig == "" {
filterConfig = "(cn=*)"
}
// Combine the default filter with our field filter, e.g. (&(cn=*)(uid=jdoe))
combinedFilter := fmt.Sprintf("(&%s%s)", filterConfig, filter)
attributes := ls.buildUserAttributes(cfg)
return ldap.NewSearchRequest(
baseDn,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0, // unlimited search results
0, // no server-side time limit
false, // typesOnly
combinedFilter,
attributes,
nil,
)
}
// buildUserAttributes returns the list of attributes we want from LDAP user searches.
func (ls *LdapService) buildUserAttributes(cfg *config.Ldap) []string {
return []string{
"dn",
ls.fieldUsername(cfg),
ls.fieldEmail(cfg),
ls.fieldFirstName(cfg),
ls.fieldLastName(cfg),
ls.fieldMemberOf(),
ls.fieldUserEnableAttr(cfg),
}
}
// userResultToLdapUser maps an *ldap.Entry to our LdapUser struct.
func (ls *LdapService) userResultToLdapUser(cfg *config.Ldap, entry *ldap.Entry) *LdapUser {
lu := &LdapUser{
Dn: entry.DN,
Username: entry.GetAttributeValue(ls.fieldUsername(cfg)),
Email: entry.GetAttributeValue(ls.fieldEmail(cfg)),
FirstName: entry.GetAttributeValue(ls.fieldFirstName(cfg)),
LastName: entry.GetAttributeValue(ls.fieldLastName(cfg)),
MemberOf: entry.GetAttributeValues(ls.fieldMemberOf()),
EnableAttrValue: entry.GetAttributeValue(ls.fieldUserEnableAttr(cfg)),
}
// Check if the user is enabled based on the LDAP configuration
ls.isUserEnabled(cfg, lu)
return lu
}
// filterField helps build simple attribute filters, e.g. (uid=username).
func (ls *LdapService) filterField(field, value string) string {
return fmt.Sprintf("(%s=%s)", field, value)
}
// fieldUsername returns the configured username attribute or "uid" if not set.
func (ls *LdapService) fieldUsername(cfg *config.Ldap) string {
if cfg.User.Username == "" {
return "uid"
}
return cfg.User.Username
}
// fieldEmail returns the configured email attribute or "mail" if not set.
func (ls *LdapService) fieldEmail(cfg *config.Ldap) string {
if cfg.User.Email == "" {
return "mail"
}
return cfg.User.Email
}
// fieldFirstName returns the configured first name attribute or "givenName" if not set.
func (ls *LdapService) fieldFirstName(cfg *config.Ldap) string {
if cfg.User.FirstName == "" {
return "givenName"
}
return cfg.User.FirstName
}
// fieldLastName returns the configured last name attribute or "sn" if not set.
func (ls *LdapService) fieldLastName(cfg *config.Ldap) string {
if cfg.User.LastName == "" {
return "sn"
}
return cfg.User.LastName
}
func (ls *LdapService) fieldMemberOf() string {
return "memberOf"
}
func (ls *LdapService) fieldUserEnableAttr(cfg *config.Ldap) string {
if cfg.User.EnableAttr == "" {
return "userAccountControl"
}
return cfg.User.EnableAttr
}
// baseDnUser returns the user-specific base DN or the global base DN if none is set.
func (ls *LdapService) baseDnUser(cfg *config.Ldap) string {
if cfg.User.BaseDn == "" {
return cfg.BaseDn
}
return cfg.User.BaseDn
}
// isUserAdmin checks if the user is a member of the admin group.
func (ls *LdapService) isUserAdmin(cfg *config.Ldap, ldapUser *LdapUser) bool {
// Check if the admin group is configured
adminGroup := cfg.User.AdminGroup
if adminGroup == "" {
return false
}
// Check "memberOf" directly
if len(ldapUser.MemberOf) > 0 {
for _, group := range ldapUser.MemberOf {
if group == adminGroup {
return true
}
}
return false
}
// For "member" attribute, perform a reverse search on the group
member := "member"
userDN := ldap.EscapeFilter(ldapUser.Dn)
adminGroupDn := ldap.EscapeFilter(adminGroup)
groupFilter := fmt.Sprintf("(%s=%s)", member, userDN)
// Create the LDAP search request
groupSearchRequest := ldap.NewSearchRequest(
adminGroupDn,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0, // Unlimited search results
0, // No time limit
false, // Return both attributes and DN
groupFilter,
[]string{"dn"},
nil,
)
// Perform the group search
groupResult, err := ls.searchResult(cfg, groupSearchRequest)
if err != nil {
return false
}
// If any results are returned, the user is part of the admin group
if len(groupResult.Entries) > 0 {
return true
}
return false
}
// isUserEnabled checks if the user is enabled based on the LDAP configuration.
// If no enable attribute or value is set, all users are considered enabled by default.
func (ls *LdapService) isUserEnabled(cfg *config.Ldap, ldapUser *LdapUser) bool {
// Retrieve the enable attribute and expected value from the configuration
enableAttr := cfg.User.EnableAttr
enableAttrValue := cfg.User.EnableAttrValue
// If no enable attribute or value is configured, consider all users as enabled
if enableAttr == "" || enableAttrValue == "" {
ldapUser.Enabled = true
return true
}
// Normalize the enable attribute for comparison
enableAttr = strings.ToLower(enableAttr)
// Handle Active Directory's userAccountControl attribute
if enableAttr == "useraccountcontrol" {
// Parse the userAccountControl value
userAccountControl, err := strconv.Atoi(ldapUser.EnableAttrValue)
if err != nil {
fmt.Printf("[ERROR] Invalid userAccountControl value: %v\n", err)
ldapUser.Enabled = false
return false
}
// Account is disabled if the ACCOUNTDISABLE flag (0x2) is set
const ACCOUNTDISABLE = 0x2
ldapUser.Enabled = (userAccountControl&ACCOUNTDISABLE == 0)
return ldapUser.Enabled
}
// For other attributes, perform a direct comparison with the expected value
ldapUser.Enabled = (ldapUser.EnableAttrValue == enableAttrValue)
return ldapUser.Enabled
}
// getAttrOfDn retrieves the value of an attribute for a given DN.
func (ls *LdapService) getAttrOfDn(cfg *config.Ldap, dn, attr string) string {
searchRequest := ldap.NewSearchRequest(
ldap.EscapeFilter(dn),
ldap.ScopeBaseObject,
ldap.NeverDerefAliases,
0, // unlimited search results
0, // no server-side time limit
false, // typesOnly
"(objectClass=*)",
[]string{attr},
nil,
)
sr, err := ls.searchResult(cfg, searchRequest)
if err != nil {
return ""
}
if len(sr.Entries) == 0 {
return ""
}
return sr.Entries[0].GetAttributeValue(attr)
}

View File

@@ -47,3 +47,12 @@ func (us *LoginLogService) Update(u *model.LoginLog) error {
func (us *LoginLogService) BatchDelete(ids []uint) error { func (us *LoginLogService) BatchDelete(ids []uint) error {
return global.DB.Where("id in (?)", ids).Delete(&model.LoginLog{}).Error return global.DB.Where("id in (?)", ids).Delete(&model.LoginLog{}).Error
} }
func (us *LoginLogService) SoftDelete(l *model.LoginLog) error {
l.IsDeleted = model.IsDeletedYes
return us.Update(l)
}
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
}

99
service/serverCmd.go Normal file
View File

@@ -0,0 +1,99 @@
package service
import (
"Gwen/global"
"Gwen/model"
"fmt"
"net"
"time"
)
type ServerCmdService struct{}
// List
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.Count(&res.Total)
tx.Scopes(Paginate(page, pageSize))
tx.Find(&res.ServerCmds)
return
}
// Info
func (is *ServerCmdService) Info(id uint) *model.ServerCmd {
u := &model.ServerCmd{}
global.DB.Where("id = ?", id).First(u)
return u
}
// Delete
func (is *ServerCmdService) Delete(u *model.ServerCmd) error {
return global.DB.Delete(u).Error
}
// Create
func (is *ServerCmdService) Create(u *model.ServerCmd) error {
res := global.DB.Create(u).Error
return res
}
// SendCmd 发送命令
func (is *ServerCmdService) SendCmd(target string, cmd string, arg string) (string, error) {
port := 0
switch target {
case model.ServerCmdTargetIdServer:
port = global.Config.Rustdesk.IdServerPort - 1
case model.ServerCmdTargetRelayServer:
port = global.Config.Rustdesk.RelayServerPort
}
//组装命令
cmd = cmd + " " + arg
res, err := is.SendSocketCmd("v6", port, cmd)
if err == nil {
return res, nil
}
//v6连接失败尝试v4
res, err = is.SendSocketCmd("v4", port, cmd)
if err == nil {
return res, nil
}
return "", err
}
// SendSocketCmd
func (is *ServerCmdService) SendSocketCmd(ty string, port int, cmd string) (string, error) {
addr := "[::1]"
tcp := "tcp6"
if ty == "v4" {
tcp = "tcp"
addr = "127.0.0.1"
}
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)
return "", err
}
defer conn.Close()
//发送命令
_, err = conn.Write([]byte(cmd))
if err != nil {
global.Logger.Debugf("%s send cmd failed: %v", ty, err)
return "", err
}
time.Sleep(100 * time.Millisecond)
//读取返回
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)
return "", err
}
return string(buf[:n]), nil
}
func (is *ServerCmdService) Update(f *model.ServerCmd) error {
return global.DB.Model(f).Updates(f).Error
}

View File

@@ -17,6 +17,8 @@ type Service struct {
*LoginLogService *LoginLogService
*AuditService *AuditService
*ShareRecordService *ShareRecordService
*ServerCmdService
*LdapService
} }
func New() *Service { func New() *Service {

View File

@@ -29,6 +29,7 @@ func (s *TagService) ListByUserId(userId uint) (res *model.TagList) {
func (s *TagService) ListByUserIdAndCollectionId(userId, cid uint) (res *model.TagList) { func (s *TagService) ListByUserIdAndCollectionId(userId, cid uint) (res *model.TagList) {
res = s.List(1, 1000, func(tx *gorm.DB) { res = s.List(1, 1000, func(tx *gorm.DB) {
tx.Where("user_id = ? and collection_id = ?", userId, cid) tx.Where("user_id = ? and collection_id = ?", userId, cid)
tx.Order("name asc")
}) })
return return
} }

View File

@@ -2,16 +2,16 @@ package service
import ( import (
"Gwen/global" "Gwen/global"
adResp "Gwen/http/response/admin"
"Gwen/model" "Gwen/model"
"Gwen/utils" "Gwen/utils"
"errors" "errors"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"math/rand" "math/rand"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
) )
type UserService struct { type UserService struct {
@@ -47,6 +47,14 @@ func (us *UserService) InfoByOpenid(openid string) *model.User {
// InfoByUsernamePassword 根据用户名密码取用户信息 // InfoByUsernamePassword 根据用户名密码取用户信息
func (us *UserService) InfoByUsernamePassword(username, password string) *model.User { func (us *UserService) InfoByUsernamePassword(username, password string) *model.User {
if global.Config.Ldap.Enable {
u, err := AllService.LdapService.Authenticate(username, password)
if err == nil {
return u
}
global.Logger.Error("LDAP authentication failed, %v", err)
global.Logger.Warn("Fallback to local database")
}
u := &model.User{} u := &model.User{}
global.DB.Where("username = ? and password = ?", username, us.EncryptPassword(password)).First(u) global.DB.Where("username = ? and password = ?", username, us.EncryptPassword(password)).First(u)
return u return u
@@ -69,6 +77,9 @@ func (us *UserService) InfoByAccessToken(token string) (*model.User, *model.User
// GenerateToken 生成token // GenerateToken 生成token
func (us *UserService) GenerateToken(u *model.User) string { func (us *UserService) GenerateToken(u *model.User) string {
if len(global.Jwt.Key) > 0 {
return global.Jwt.GenerateToken(u.Id)
}
return utils.Md5(u.Username + time.Now().String()) return utils.Md5(u.Username + time.Now().String())
} }
@@ -80,7 +91,7 @@ func (us *UserService) Login(u *model.User, llog *model.LoginLog) *model.UserTok
Token: token, Token: token,
DeviceUuid: llog.Uuid, DeviceUuid: llog.Uuid,
DeviceId: llog.DeviceId, DeviceId: llog.DeviceId,
ExpiredAt: time.Now().Add(time.Hour * 24 * 7).Unix(), ExpiredAt: us.UserTokenExpireTimestamp(),
} }
global.DB.Create(ut) global.DB.Create(ut)
llog.UserTokenId = ut.UserId llog.UserTokenId = ut.UserId
@@ -154,6 +165,9 @@ func (us *UserService) CheckUserEnable(u *model.User) bool {
// Create 创建 // Create 创建
func (us *UserService) Create(u *model.User) error { func (us *UserService) Create(u *model.User) error {
// The initial username should be formatted, and the username should be unique // The initial username should be formatted, and the username should be unique
if us.IsUsernameExists(u.Username) {
return errors.New("UsernameExists")
}
u.Username = us.formatUsername(u.Username) u.Username = us.formatUsername(u.Username)
u.Password = us.EncryptPassword(u.Password) u.Password = us.EncryptPassword(u.Password)
res := global.DB.Create(u).Error res := global.DB.Create(u).Error
@@ -272,9 +286,9 @@ func (us *UserService) IsAdmin(u *model.User) bool {
// RouteNames // RouteNames
func (us *UserService) RouteNames(u *model.User) []string { func (us *UserService) RouteNames(u *model.User) []string {
if us.IsAdmin(u) { if us.IsAdmin(u) {
return adResp.AdminRouteNames return model.AdminRouteNames
} }
return adResp.UserRouteNames return model.UserRouteNames
} }
// InfoByOauthId 根据oauth的name和openId取用户信息 // InfoByOauthId 根据oauth的name和openId取用户信息
@@ -309,7 +323,16 @@ func (us *UserService) RegisterByOauth(oauthUser *model.OauthUser, op string) (e
email = strings.ToLower(email) email = strings.ToLower(email)
// update email to oauthUser, in case it contain upper case // update email to oauthUser, in case it contain upper case
oauthUser.Email = email oauthUser.Email = email
user := us.InfoByEmail(email) // call this, if find user by email, it will update the email to local database
user, ldapErr := AllService.LdapService.GetUserInfoByEmailLocal(email)
// If we enable ldap, and the error is not ErrLdapUserNotFound, return the error because we could not sure if the user is not found in ldap
if !(errors.Is(ldapErr, ErrLdapNotEnabled) || errors.Is(ldapErr, ErrLdapUserNotFound) || ldapErr == nil) {
return ldapErr, user
}
if user.Id == 0 {
// this means the user is not found in ldap, maybe ldao is not enabled
user = us.InfoByEmail(email)
}
if user.Id != 0 { if user.Id != 0 {
ut.FromOauthUser(user.Id, oauthUser, oauthType, op) ut.FromOauthUser(user.Id, oauthUser, oauthType, op)
global.DB.Create(ut) global.DB.Create(ut)
@@ -341,13 +364,10 @@ func (us *UserService) RegisterByOauth(oauthUser *model.OauthUser, op string) (e
// GenerateUsernameByOauth 生成用户名 // GenerateUsernameByOauth 生成用户名
func (us *UserService) GenerateUsernameByOauth(name string) string { func (us *UserService) GenerateUsernameByOauth(name string) string {
u := &model.User{} for us.IsUsernameExists(name) {
global.DB.Where("username = ?", name).First(u) name += strconv.Itoa(rand.Intn(10)) // Append a random digit (0-9)
if u.Id == 0 {
return name
} }
name = name + strconv.FormatInt(rand.Int63n(10), 10) return name
return us.GenerateUsernameByOauth(name)
} }
// UserThirdsByUserId // UserThirdsByUserId
@@ -392,15 +412,18 @@ func (us *UserService) IsPasswordEmptyByUser(u *model.User) bool {
return us.IsPasswordEmptyById(u.Id) return us.IsPasswordEmptyById(u.Id)
} }
// Register 注册 // Register 注册, 如果用户名已存在则返回nil
func (us *UserService) Register(username string, email string, password string) *model.User { func (us *UserService) Register(username string, email string, password string) *model.User {
u := &model.User{ u := &model.User{
Username: username, Username: username,
Email: email, Email: email,
Password: us.EncryptPassword(password), Password: password,
GroupId: 1, GroupId: 1,
} }
global.DB.Create(u) err := us.Create(u)
if err != nil {
return nil
}
return u return u
} }
@@ -449,8 +472,17 @@ func (us *UserService) getAdminUserCount() int64 {
return count return count
} }
// UserTokenExpireTimestamp 生成用户token过期时间
func (us *UserService) UserTokenExpireTimestamp() int64 {
exp := global.Config.App.TokenExpire
if exp == 0 {
exp = 3600 * 24 * 7
}
return time.Now().Add(time.Second * time.Duration(exp)).Unix()
}
func (us *UserService) RefreshAccessToken(ut *model.UserToken) { func (us *UserService) RefreshAccessToken(ut *model.UserToken) {
ut.ExpiredAt = time.Now().Add(time.Hour * 24 * 7).Unix() ut.ExpiredAt = us.UserTokenExpireTimestamp()
global.DB.Model(ut).Update("expired_at", ut.ExpiredAt) global.DB.Model(ut).Update("expired_at", ut.ExpiredAt)
} }
func (us *UserService) AutoRefreshAccessToken(ut *model.UserToken) { func (us *UserService) AutoRefreshAccessToken(ut *model.UserToken) {
@@ -462,3 +494,22 @@ func (us *UserService) AutoRefreshAccessToken(ut *model.UserToken) {
func (us *UserService) BatchDeleteUserToken(ids []uint) error { func (us *UserService) BatchDeleteUserToken(ids []uint) error {
return global.DB.Where("id in ?", ids).Delete(&model.UserToken{}).Error return global.DB.Where("id in ?", ids).Delete(&model.UserToken{}).Error
} }
func (us *UserService) VerifyJWT(token string) (uint, error) {
return global.Jwt.ParseToken(token)
}
// IsUsernameExists 判断用户名是否存在, it will check the internal database and LDAP(if enabled)
func (us *UserService) IsUsernameExists(username string) bool {
return us.IsUsernameExistsLocal(username) || AllService.LdapService.IsUsernameExists(username)
}
func (us *UserService) IsUsernameExistsLocal(username string) bool {
u := &model.User{}
global.DB.Where("username = ?", username).First(u)
return u.Id != 0
}
func (us *UserService) IsEmailExistsLdap(email string) bool {
return AllService.LdapService.IsEmailExists(email)
}

View File

@@ -0,0 +1,18 @@
[Unit]
Description=Rustdesk api Server
[Service]
Type=simple
LimitNOFILE=1000000
ExecStart=/usr/bin/rustdesk-api
WorkingDirectory=/var/lib/rustdesk-api/
User=
Group=
Restart=always
StandardOutput=append:/var/log/rustdesk-api/rustdesk-api.log
StandardError=append:/var/log/rustdesk-api/rustdesk-api.error
# Restart service after 10 seconds if node service crashes
RestartSec=10
[Install]
WantedBy=multi-user.target