This commit is contained in:
ljw
2024-09-13 15:57:29 +08:00
commit c53df223d1
112 changed files with 14353 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
.idea
runtime
!runtime/cache/.gitignore
go.sum
resources
!resources/public/upload/.gitignore
!resources/public/web
release
data

105
README.md Normal file
View File

@@ -0,0 +1,105 @@
# RustDesk API
本项目使用 Go 实现了 RustDesk 的 API并包含了 Web UI 和 Web 客户端。RustDesk 是一个远程桌面软件,提供了自托管的解决方案。
## 使用前准备
### Rustdesk
1. PC客户端使用的是 ***1.3.0***,经测试 ***1.2.6+*** 都可以
2. server端必须指定key不能用自带的生成的key,否则可能链接不上或者超时
```bash
hbbs -r <relay-server-ip[:port]> -k 123456789
hbbr -k 123456789
```
## 功能
### **API 服务**: 基本实现了PC端基础的接口。
![pc_ab](docs/pc_ab.png)
![pc_gr](docs/pc_gr.png)
### **Web UI**: 使用前后端分离,提供用户友好的管理界面,主要用来管理和展示。
***初次安装管理员为用户名密码为admin admin请即时更改密码***
1. 管理员界面
![web_admin](docs/web_admin.png)
2. 普通用户界面
![web_user](docs/web_user.png)
3. 更改密码在右上角
![web_resetpwd](docs/web_resetpwd.png)
4. 分组可以自定义,方便管理,暂时支持两种类型: `共享组``普通组`
![web_admin_gr](docs/web_admin_gr.png)
### **Web 客户端**:
1. 如果已经登录了后台web client将自动直接登录
2. 如果没登录后台点击右上角登录即可api server已经自动配置好了
3. 登录后台后会将地址簿自动保存到web client中方便使用
![webclient_conf](docs/webclient_conf.png)
### **自动化文档**: 使用 Swag 生成 API 文档,方便开发者理解和使用 API。
1. 后台文档 <youer server>/admin/swagger/index.html
2. PC端文档 <youer server>/swagger/index.html
![api_swag](docs/api_swag.png)
## 安装与运行
### 相关配置
* 参考`conf/config.yaml`配置文件,修改相关配置。如果`gorm.type``sqlite`则不需要配置mysql相关配置。
```yaml
gin:
api-addr: "0.0.0.0:21114"
mode: "release"
resources-path: 'resources'
gorm:
type: "sqlite"
max-idle-conns: 10
max-open-conns: 100
mysql:
username: "root"
password: "111111"
addr: "192.168.1.66:3308"
dbname: "rustdesk"
rustdesk:
id-server: "192.168.1.66:21116"
relay-server: "192.168.1.66:21117"
api-server: "http://192.168.1.66:21114"
key: "123456789"
```
### 安装步骤
#### docker运行
#### 下载release直接运行
#### 源码安装
1. 克隆仓库
```bash
git clone https://github.com/lejianwen/rustdesk-api.git
cd rustdesk-api
```
2. 安装依赖
```bash
go mod tidy
```
3. 运行
```bash
go run cmd/apimain.go
#或者直接Build
./build.sh
#或者使用generate_api.go生成api
go generate generate_api.go
```
4. 编译,如果想自己编译,先cd到项目根目录然后windows下直接运行`build.bat`,linux下运行`build.sh`,编译后会在`release`
目录下生成对应的可执行文件。

14
build.bat Normal file
View File

@@ -0,0 +1,14 @@
@echo off
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
go env -w CGO_ENABLED=1
go env -w GOOS=windows
go env -w GOARCH=amd64
swag init -g cmd/apimain.go --output docs/api --instanceName api --exclude http/controller/admin
swag init -g cmd/apimain.go --output docs/admin --instanceName admin --exclude http/controller/api
go build -o release/apimain.exe cmd/apimain.go
xcopy resources release\resources /E /I /Y
xcopy docs release\docs /E /I /Y
xcopy data release\data /E /I /Y
xcopy conf release\conf /E /I /Y
xcopy runtime release\runtime /E /I /Y

15
build.sh Normal file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
go env -w CGO_ENABLED=1
go env -w GOOS=linux
go env -w GOARCH=amd64
swag init -g cmd/apimain.go --output docs/api --instanceName api --exclude http/controller/admin
swag init -g cmd/apimain.go --output docs/admin --instanceName admin --exclude http/controller/api
go build -o release/apimain cmd/apimain.go
cp -ar resources release/
cp -ar docs release/
cp -ar data release/
cp -ar conf release/
cp -ar runtime release/

252
cmd/apimain.go Normal file
View File

@@ -0,0 +1,252 @@
package main
import (
"Gwen/config"
"Gwen/global"
"Gwen/http"
"Gwen/lib/cache"
"Gwen/lib/lock"
"Gwen/lib/logger"
"Gwen/lib/orm"
"Gwen/lib/upload"
"Gwen/model"
"Gwen/service"
"fmt"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh_Hans_CN"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
zh_translations "github.com/go-playground/validator/v10/translations/zh"
"github.com/go-redis/redis/v8"
"reflect"
)
// @title 管理系统API
// @version 1.0
// @description 接口
// @basePath /api
// @securityDefinitions.apikey token
// @in header
// @name api-token
// @securitydefinitions.apikey BearerAuth
// @in header
// @name Authorization
func main() {
//配置解析
global.Viper = config.Init(&global.Config, func() {
fmt.Println(global.Config)
})
//日志
global.Logger = logger.New(&logger.Config{
Path: global.Config.Logger.Path,
Level: global.Config.Logger.Level,
ReportCaller: global.Config.Logger.ReportCaller,
})
//redis
global.Redis = redis.NewClient(&redis.Options{
Addr: global.Config.Redis.Addr,
Password: global.Config.Redis.Password,
DB: global.Config.Redis.Db,
})
//cache
if global.Config.Cache.Type == cache.TypeFile {
fc := cache.NewFileCache()
fc.SetDir(global.Config.Cache.FileDir)
global.Cache = fc
} else if global.Config.Cache.Type == cache.TypeRedis {
global.Cache = cache.NewRedis(&redis.Options{
Addr: global.Config.Cache.RedisAddr,
Password: global.Config.Cache.RedisPwd,
DB: global.Config.Cache.RedisDb,
})
}
//gorm
if global.Config.Gorm.Type == config.TypeMysql {
dns := global.Config.Mysql.Username + ":" + global.Config.Mysql.Password + "@(" + global.Config.Mysql.Addr + ")/" + global.Config.Mysql.Dbname + "?charset=utf8mb4&parseTime=True&loc=Local"
global.DB = orm.NewMysql(&orm.MysqlConfig{
Dns: dns,
MaxIdleConns: global.Config.Gorm.MaxIdleConns,
MaxOpenConns: global.Config.Gorm.MaxOpenConns,
})
} else {
//sqlite
global.DB = orm.NewSqlite(&orm.SqliteConfig{
MaxIdleConns: global.Config.Gorm.MaxIdleConns,
MaxOpenConns: global.Config.Gorm.MaxOpenConns,
})
}
DatabaseAutoUpdate()
//validator
ApiInitValidator()
//oss
global.Oss = &upload.Oss{
AccessKeyId: global.Config.Oss.AccessKeyId,
AccessKeySecret: global.Config.Oss.AccessKeySecret,
Host: global.Config.Oss.Host,
CallbackUrl: global.Config.Oss.CallbackUrl,
ExpireTime: global.Config.Oss.ExpireTime,
MaxByte: global.Config.Oss.MaxByte,
}
//jwt
//fmt.Println(global.Config.Jwt.PrivateKey)
//global.Jwt = jwt.NewJwt(global.Config.Jwt.PrivateKey, global.Config.Jwt.ExpireDuration*time.Second)
//locker
global.Lock = lock.NewLocal()
//gin
http.ApiInit()
}
func ApiInitValidator() {
validate := validator.New()
enT := en.New()
cn := zh_Hans_CN.New()
uni := ut.New(enT, cn)
trans, _ := uni.GetTranslator("cn")
err := zh_translations.RegisterDefaultTranslations(validate, trans)
if err != nil {
//退出
panic(err)
}
validate.RegisterTagNameFunc(func(field reflect.StructField) string {
label := field.Tag.Get("label")
if label == "" {
return field.Name
}
return label
})
global.Validator.Validate = validate
global.Validator.VTrans = trans
global.Validator.ValidStruct = func(i interface{}) []string {
err := global.Validator.Validate.Struct(i)
errList := make([]string, 0, 10)
if err != nil {
if _, ok := err.(*validator.InvalidValidationError); ok {
errList = append(errList, err.Error())
return errList
}
for _, err2 := range err.(validator.ValidationErrors) {
errList = append(errList, err2.Translate(global.Validator.VTrans))
}
}
return errList
}
global.Validator.ValidVar = func(field interface{}, tag string) []string {
err := global.Validator.Validate.Var(field, tag)
fmt.Println(err)
errList := make([]string, 0, 10)
if err != nil {
if _, ok := err.(*validator.InvalidValidationError); ok {
errList = append(errList, err.Error())
return errList
}
for _, err2 := range err.(validator.ValidationErrors) {
errList = append(errList, err2.Translate(global.Validator.VTrans))
}
}
return errList
}
}
func DatabaseAutoUpdate() {
version := 100
db := global.DB
if global.Config.Gorm.Type == config.TypeMysql {
//检查存不存在数据库,不存在则创建
dbName := db.Migrator().CurrentDatabase()
fmt.Println("dbName", dbName)
if dbName == "" {
dbName = global.Config.Mysql.Dbname
// 移除 DSN 中的数据库名称,以便初始连接时不指定数据库
dsnWithoutDB := global.Config.Mysql.Username + ":" + global.Config.Mysql.Password + "@(" + global.Config.Mysql.Addr + ")/?charset=utf8mb4&parseTime=True&loc=Local"
//新链接
dbWithoutDB := orm.NewMysql(&orm.MysqlConfig{
Dns: dsnWithoutDB,
})
// 获取底层的 *sql.DB 对象,并确保在程序退出时关闭连接
sqlDBWithoutDB, err := dbWithoutDB.DB()
if err != nil {
fmt.Printf("获取底层 *sql.DB 对象失败: %v\n", err)
return
}
defer func() {
if err := sqlDBWithoutDB.Close(); err != nil {
fmt.Printf("关闭连接失败: %v\n", err)
}
}()
err = dbWithoutDB.Exec("CREATE DATABASE IF NOT EXISTS " + dbName + " DEFAULT CHARSET utf8mb4").Error
if err != nil {
fmt.Println(err)
return
}
}
}
if !db.Migrator().HasTable(&model.Version{}) {
Migrate(uint(version))
} else {
//查找最后一个version
var v model.Version
db.Last(&v)
if v.Version < uint(version) {
Migrate(uint(version))
}
}
}
func Migrate(version uint) {
fmt.Println("migrating....", version)
err := global.DB.AutoMigrate(
&model.Version{},
&model.User{},
&model.UserToken{},
&model.Tag{},
&model.AddressBook{},
&model.Peer{},
&model.Group{},
)
if err != nil {
fmt.Println("migrate err :=>", err)
}
global.DB.Create(&model.Version{Version: version})
//如果是初次则创建一个默认用户
var vc int64
global.DB.Model(&model.Version{}).Count(&vc)
if vc == 1 {
group := &model.Group{
Name: "默认组",
Type: model.GroupTypeDefault,
}
service.AllService.GroupService.Create(group)
groupShare := &model.Group{
Name: "共享组",
Type: model.GroupTypeShare,
}
service.AllService.GroupService.Create(groupShare)
//是true
is_admin := true
admin := &model.User{
Username: "admin",
Nickname: "管理员",
Status: model.COMMON_STATUS_ENABLE,
IsAdmin: &is_admin,
GroupId: 1,
}
admin.Password = service.AllService.UserService.EncryptPassword("admin")
global.DB.Create(admin)
}
}

42
conf/config.yaml Normal file
View File

@@ -0,0 +1,42 @@
gin:
api-addr: "0.0.0.0:21114"
mode: "release" #release,debug,test
resources-path: 'resources' #对外静态文件目录
gorm:
type: "sqlite"
max-idle-conns: 10
max-open-conns: 100
mysql:
username: "root"
password: "111111"
addr: "192.168.1.66:3308"
dbname: "rustdesk2"
rustdesk:
id-server: "124.220.134.240:21116"
relay-server: "124.220.134.240:21117"
api-server: "http://127.0.0.1:21114"
key: "ljw19891989"
redis:
addr: "127.0.0.1:6379"
password: ""
db: 0
logger:
path: "./runtime/log.txt"
level: "error" #trace,debug,info,warn,error,fatal
report-caller: true
cache:
type: "file"
file-dir: "./runtime/cache"
redis-addr: "127.0.0.1:6379"
redis-pwd: "ljw19891989"
redis-db: 0
oss:
access-key-id: ""
access-key-secret: ""
host: ""
callback-url: ""
expire-time: 30
max-byte: 10240
jwt:
private-key: "./conf/jwt_pri.pem"
expire-duration: 360000

0
conf/jwt_pri.pem Normal file
View File

9
config/cache.go Normal file
View File

@@ -0,0 +1,9 @@
package config
type Cache struct {
Type string
RedisAddr string `mapstructure:"redis-addr"`
RedisPwd string `mapstructure:"redis-pwd"`
RedisDb int `mapstructure:"redis-db"`
FileDir string `mapstructure:"file-dir"`
}

56
config/config.go Normal file
View File

@@ -0,0 +1,56 @@
package config
import (
"flag"
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)
const (
DebugMode = "debug"
ReleaseMode = "release"
DefaultConfig = "conf/config.yaml"
)
type Config struct {
Gorm Gorm
Mysql Mysql
Gin Gin
Logger Logger
Redis Redis
Cache Cache
Oss Oss
Jwt Jwt
Rustdesk Rustdesk
}
// Init 初始化配置
func Init(rowVal interface{}, cb func()) *viper.Viper {
var config string
flag.StringVar(&config, "c", "", "choose config file.")
flag.Parse()
if config == "" { // 优先级: 命令行 > 默认值
config = DefaultConfig
}
v := viper.New()
v.SetConfigFile(config)
v.SetConfigType("yaml")
err := v.ReadInConfig()
if err != nil {
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}
v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
//配置文件修改监听
fmt.Println("config file changed:", e.Name)
if err2 := v.Unmarshal(rowVal); err2 != nil {
fmt.Println(err2)
}
cb()
})
if err := v.Unmarshal(rowVal); err != nil {
fmt.Println(err)
}
return v
}

8
config/gin.go Normal file
View File

@@ -0,0 +1,8 @@
package config
type Gin struct {
ApiAddr string `mapstructure:"api-addr"`
AdminAddr string `mapstructure:"admin-addr"`
Mode string
ResourcesPath string `mapstructure:"resources-path"`
}

19
config/gorm.go Normal file
View File

@@ -0,0 +1,19 @@
package config
const (
TypeSqlite = "sqlite"
TypeMysql = "mysql"
)
type Gorm struct {
Type string `mapstructure:"type"`
MaxIdleConns int `mapstructure:"max-idle-conns"`
MaxOpenConns int `mapstructure:"max-open-conns"`
}
type Mysql struct {
Addr string `mapstructure:"addr"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Dbname string `mapstructure:"dbname"`
}

8
config/jwt.go Normal file
View File

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

7
config/logger.go Normal file
View File

@@ -0,0 +1,7 @@
package config
type Logger struct {
Path string
Level string
ReportCaller bool `mapstructure:"report-caller"`
}

10
config/oss.go Normal file
View File

@@ -0,0 +1,10 @@
package config
type Oss struct {
AccessKeyId string `mapstructure:"access-key-id"`
AccessKeySecret string `mapstructure:"access-key-secret"`
Host string `mapstructure:"host"`
CallbackUrl string `mapstructure:"callback-url"`
ExpireTime int64 `mapstructure:"expire-time"`
MaxByte int64 `mapstructure:"max-byte"`
}

7
config/redis.go Normal file
View File

@@ -0,0 +1,7 @@
package config
type Redis struct {
Addr string
Password string
Db int
}

8
config/rustdesk.go Normal file
View File

@@ -0,0 +1,8 @@
package config
type Rustdesk struct {
IdServer string `mapstructure:"id-server"`
RelayServer string `mapstructure:"relay-server"`
ApiServer string `mapstructure:"api-server"`
Key string `mapstructure:"key"`
}

2331
docs/admin/admin_docs.go Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

830
docs/api/api_docs.go Normal file
View File

@@ -0,0 +1,830 @@
// Package api Code generated by swaggo/swag. DO NOT EDIT
package api
import "github.com/swaggo/swag"
const docTemplateapi = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/": {
"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"
}
}
}
}
},
"/ab": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"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.ErrorResponse"
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "地址更新",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"地址"
],
"summary": "地址更新",
"parameters": [
{
"description": "地址表单",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.AddressBookForm"
}
}
],
"responses": {
"200": {
"description": "null",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/ab/add": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "标签",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"地址"
],
"summary": "标签添加",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/ab/personal": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "个人信息",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"用户"
],
"summary": "个人信息",
"parameters": [
{
"description": "string valid",
"name": "string",
"in": "body",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/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"
}
}
}
}
},
"/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": {
"post": {
"description": "心跳",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"首页"
],
"summary": "心跳",
"responses": {
"200": {
"description": "OK"
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/login": {
"post": {
"description": "登录",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"登录"
],
"summary": "登录",
"parameters": [
{
"description": "登录表单",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.LoginForm"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.LoginRes"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/login-options": {
"post": {
"description": "登录选项",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"登录"
],
"summary": "登录选项",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/logout": {
"post": {
"description": "登出",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"登录"
],
"summary": "登出",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/peers": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "机器",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"群组"
],
"summary": "机器",
"parameters": [
{
"type": "integer",
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "每页数量",
"name": "pageSize",
"in": "query"
},
{
"type": "integer",
"description": "状态",
"name": "status",
"in": "query"
},
{
"type": "string",
"description": "accessible",
"name": "accessible",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.DataResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/server-config": {
"get": {
"security": [
{
"token": []
}
],
"description": "服务配置,给webclient提供api-server",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"WEBCLIENT"
],
"summary": "服务配置",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/sysinfo": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "提交系统信息",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"地址"
],
"summary": "提交系统信息",
"parameters": [
{
"description": "系统信息表单",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.PeerForm"
}
}
],
"responses": {
"200": {
"description": "SYSINFO_UPDATED,ID_NOT_FOUND",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/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": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "用户列表",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"群组"
],
"summary": "用户列表",
"parameters": [
{
"type": "integer",
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "每页数量",
"name": "pageSize",
"in": "query"
},
{
"type": "integer",
"description": "状态",
"name": "status",
"in": "query"
},
{
"type": "string",
"description": "accessible",
"name": "accessible",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.DataResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/api.UserPayload"
}
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
}
},
"definitions": {
"api.AddressBookForm": {
"type": "object",
"properties": {
"data": {
"type": "string",
"example": "{\"tags\":[\"tag1\",\"tag2\",\"tag3\"],\"peers\":[{\"id\":\"abc\",\"username\":\"abv-l\",\"hostname\":\"\",\"platform\":\"Windows\",\"alias\":\"\",\"tags\":[\"tag1\",\"tag2\"],\"hash\":\"hash\"}],\"tag_colors\":\"{\\\"tag1\\\":4288585374,\\\"tag2\\\":4278238420,\\\"tag3\\\":4291681337}\"}"
}
}
},
"api.LoginForm": {
"type": "object",
"required": [
"username"
],
"properties": {
"password": {
"type": "string",
"maxLength": 20,
"minLength": 4
},
"username": {
"type": "string",
"maxLength": 10,
"minLength": 4
}
}
},
"api.LoginRes": {
"type": "object",
"properties": {
"access_token": {
"type": "string"
},
"secret": {
"type": "string"
},
"tfa_type": {
"type": "string"
},
"type": {
"type": "string"
},
"user": {
"$ref": "#/definitions/api.UserPayload"
}
}
},
"api.PeerForm": {
"type": "object",
"properties": {
"cpu": {
"type": "string"
},
"hostname": {
"type": "string"
},
"id": {
"type": "string"
},
"memory": {
"type": "string"
},
"os": {
"type": "string"
},
"username": {
"type": "string"
},
"uuid": {
"type": "string"
},
"version": {
"type": "string"
}
}
},
"api.UserPayload": {
"type": "object",
"properties": {
"email": {
"type": "string"
},
"is_admin": {
"type": "boolean"
},
"name": {
"type": "string"
},
"note": {
"type": "string"
},
"status": {
"type": "integer"
}
}
},
"model.Tag": {
"type": "object",
"properties": {
"color": {
"description": "color 是flutter的颜色值,从0x00000000 到 0xFFFFFFFF; 前两位表示透明度后面6位表示颜色, 可以转成rgba",
"type": "integer"
},
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"user_id": {
"type": "integer"
}
}
},
"response.DataResponse": {
"type": "object",
"properties": {
"data": {},
"total": {
"type": "integer"
}
}
},
"response.ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
},
"response.Response": {
"type": "object",
"properties": {
"code": {
"type": "integer"
},
"data": {},
"message": {
"type": "string"
}
}
}
},
"securityDefinitions": {
"BearerAuth": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
},
"token": {
"type": "apiKey",
"name": "api-token",
"in": "header"
}
}
}`
// SwaggerInfoapi holds exported Swagger Info so clients can modify it
var SwaggerInfoapi = &swag.Spec{
Version: "1.0",
Host: "",
BasePath: "/api",
Schemes: []string{},
Title: "管理系统API",
Description: "接口",
InfoInstanceName: "api",
SwaggerTemplate: docTemplateapi,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swag.Register(SwaggerInfoapi.InstanceName(), SwaggerInfoapi)
}

805
docs/api/api_swagger.json Normal file
View File

@@ -0,0 +1,805 @@
{
"swagger": "2.0",
"info": {
"description": "接口",
"title": "管理系统API",
"contact": {},
"version": "1.0"
},
"basePath": "/api",
"paths": {
"/": {
"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"
}
}
}
}
},
"/ab": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"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.ErrorResponse"
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "地址更新",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"地址"
],
"summary": "地址更新",
"parameters": [
{
"description": "地址表单",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.AddressBookForm"
}
}
],
"responses": {
"200": {
"description": "null",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/ab/add": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "标签",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"地址"
],
"summary": "标签添加",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/ab/personal": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "个人信息",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"用户"
],
"summary": "个人信息",
"parameters": [
{
"description": "string valid",
"name": "string",
"in": "body",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/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"
}
}
}
}
},
"/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": {
"post": {
"description": "心跳",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"首页"
],
"summary": "心跳",
"responses": {
"200": {
"description": "OK"
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/login": {
"post": {
"description": "登录",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"登录"
],
"summary": "登录",
"parameters": [
{
"description": "登录表单",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.LoginForm"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.LoginRes"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/login-options": {
"post": {
"description": "登录选项",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"登录"
],
"summary": "登录选项",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/logout": {
"post": {
"description": "登出",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"登录"
],
"summary": "登出",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/peers": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "机器",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"群组"
],
"summary": "机器",
"parameters": [
{
"type": "integer",
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "每页数量",
"name": "pageSize",
"in": "query"
},
{
"type": "integer",
"description": "状态",
"name": "status",
"in": "query"
},
{
"type": "string",
"description": "accessible",
"name": "accessible",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.DataResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/server-config": {
"get": {
"security": [
{
"token": []
}
],
"description": "服务配置,给webclient提供api-server",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"WEBCLIENT"
],
"summary": "服务配置",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/sysinfo": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "提交系统信息",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"地址"
],
"summary": "提交系统信息",
"parameters": [
{
"description": "系统信息表单",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.PeerForm"
}
}
],
"responses": {
"200": {
"description": "SYSINFO_UPDATED,ID_NOT_FOUND",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/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": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "用户列表",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"群组"
],
"summary": "用户列表",
"parameters": [
{
"type": "integer",
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "每页数量",
"name": "pageSize",
"in": "query"
},
{
"type": "integer",
"description": "状态",
"name": "status",
"in": "query"
},
{
"type": "string",
"description": "accessible",
"name": "accessible",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.DataResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/api.UserPayload"
}
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
}
},
"definitions": {
"api.AddressBookForm": {
"type": "object",
"properties": {
"data": {
"type": "string",
"example": "{\"tags\":[\"tag1\",\"tag2\",\"tag3\"],\"peers\":[{\"id\":\"abc\",\"username\":\"abv-l\",\"hostname\":\"\",\"platform\":\"Windows\",\"alias\":\"\",\"tags\":[\"tag1\",\"tag2\"],\"hash\":\"hash\"}],\"tag_colors\":\"{\\\"tag1\\\":4288585374,\\\"tag2\\\":4278238420,\\\"tag3\\\":4291681337}\"}"
}
}
},
"api.LoginForm": {
"type": "object",
"required": [
"username"
],
"properties": {
"password": {
"type": "string",
"maxLength": 20,
"minLength": 4
},
"username": {
"type": "string",
"maxLength": 10,
"minLength": 4
}
}
},
"api.LoginRes": {
"type": "object",
"properties": {
"access_token": {
"type": "string"
},
"secret": {
"type": "string"
},
"tfa_type": {
"type": "string"
},
"type": {
"type": "string"
},
"user": {
"$ref": "#/definitions/api.UserPayload"
}
}
},
"api.PeerForm": {
"type": "object",
"properties": {
"cpu": {
"type": "string"
},
"hostname": {
"type": "string"
},
"id": {
"type": "string"
},
"memory": {
"type": "string"
},
"os": {
"type": "string"
},
"username": {
"type": "string"
},
"uuid": {
"type": "string"
},
"version": {
"type": "string"
}
}
},
"api.UserPayload": {
"type": "object",
"properties": {
"email": {
"type": "string"
},
"is_admin": {
"type": "boolean"
},
"name": {
"type": "string"
},
"note": {
"type": "string"
},
"status": {
"type": "integer"
}
}
},
"model.Tag": {
"type": "object",
"properties": {
"color": {
"description": "color 是flutter的颜色值,从0x00000000 到 0xFFFFFFFF; 前两位表示透明度后面6位表示颜色, 可以转成rgba",
"type": "integer"
},
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"user_id": {
"type": "integer"
}
}
},
"response.DataResponse": {
"type": "object",
"properties": {
"data": {},
"total": {
"type": "integer"
}
}
},
"response.ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
},
"response.Response": {
"type": "object",
"properties": {
"code": {
"type": "integer"
},
"data": {},
"message": {
"type": "string"
}
}
}
},
"securityDefinitions": {
"BearerAuth": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
},
"token": {
"type": "apiKey",
"name": "api-token",
"in": "header"
}
}
}

513
docs/api/api_swagger.yaml Normal file
View File

@@ -0,0 +1,513 @@
basePath: /api
definitions:
api.AddressBookForm:
properties:
data:
example: '{"tags":["tag1","tag2","tag3"],"peers":[{"id":"abc","username":"abv-l","hostname":"","platform":"Windows","alias":"","tags":["tag1","tag2"],"hash":"hash"}],"tag_colors":"{\"tag1\":4288585374,\"tag2\":4278238420,\"tag3\":4291681337}"}'
type: string
type: object
api.LoginForm:
properties:
password:
maxLength: 20
minLength: 4
type: string
username:
maxLength: 10
minLength: 4
type: string
required:
- username
type: object
api.LoginRes:
properties:
access_token:
type: string
secret:
type: string
tfa_type:
type: string
type:
type: string
user:
$ref: '#/definitions/api.UserPayload'
type: object
api.PeerForm:
properties:
cpu:
type: string
hostname:
type: string
id:
type: string
memory:
type: string
os:
type: string
username:
type: string
uuid:
type: string
version:
type: string
type: object
api.UserPayload:
properties:
email:
type: string
is_admin:
type: boolean
name:
type: string
note:
type: string
status:
type: integer
type: object
model.Tag:
properties:
color:
description: color 是flutter的颜色值,从0x00000000 到 0xFFFFFFFF; 前两位表示透明度后面6位表示颜色,
可以转成rgba
type: integer
created_at:
type: string
id:
type: integer
name:
type: string
updated_at:
type: string
user_id:
type: integer
type: object
response.DataResponse:
properties:
data: {}
total:
type: integer
type: object
response.ErrorResponse:
properties:
error:
type: string
type: object
response.Response:
properties:
code:
type: integer
data: {}
message:
type: string
type: object
info:
contact: {}
description: 接口
title: 管理系统API
version: "1.0"
paths:
/:
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:
- 首页
/ab:
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.ErrorResponse'
security:
- BearerAuth: []
summary: 地址列表
tags:
- 地址
post:
consumes:
- application/json
description: 地址更新
parameters:
- description: 地址表单
in: body
name: body
required: true
schema:
$ref: '#/definitions/api.AddressBookForm'
produces:
- application/json
responses:
"200":
description: "null"
schema:
type: string
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
security:
- BearerAuth: []
summary: 地址更新
tags:
- 地址
/ab/add:
post:
consumes:
- application/json
description: 标签
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
security:
- BearerAuth: []
summary: 标签添加
tags:
- 地址
/ab/personal:
post:
consumes:
- application/json
description: 个人信息
parameters:
- description: string valid
in: body
name: string
schema:
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
security:
- BearerAuth: []
summary: 个人信息
tags:
- 用户
/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:
- 用户
/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:
post:
consumes:
- application/json
description: 心跳
produces:
- application/json
responses:
"200":
description: OK
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
summary: 心跳
tags:
- 首页
/login:
post:
consumes:
- application/json
description: 登录
parameters:
- description: 登录表单
in: body
name: body
required: true
schema:
$ref: '#/definitions/api.LoginForm'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.LoginRes'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: 登录
tags:
- 登录
/login-options:
post:
consumes:
- application/json
description: 登录选项
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
type: string
type: array
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: 登录选项
tags:
- 登录
/logout:
post:
consumes:
- application/json
description: 登出
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: 登出
tags:
- 登录
/peers:
get:
consumes:
- application/json
description: 机器
parameters:
- description: 页码
in: query
name: page
type: integer
- description: 每页数量
in: query
name: pageSize
type: integer
- description: 状态
in: query
name: status
type: integer
- description: accessible
in: query
name: accessible
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.DataResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
security:
- BearerAuth: []
summary: 机器
tags:
- 群组
/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: 服务配置
tags:
- WEBCLIENT
/sysinfo:
post:
consumes:
- application/json
description: 提交系统信息
parameters:
- description: 系统信息表单
in: body
name: body
required: true
schema:
$ref: '#/definitions/api.PeerForm'
produces:
- application/json
responses:
"200":
description: SYSINFO_UPDATED,ID_NOT_FOUND
schema:
type: string
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
security:
- BearerAuth: []
summary: 提交系统信息
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:
get:
consumes:
- application/json
description: 用户列表
parameters:
- description: 页码
in: query
name: page
type: integer
- description: 每页数量
in: query
name: pageSize
type: integer
- description: 状态
in: query
name: status
type: integer
- description: accessible
in: query
name: accessible
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.DataResponse'
- properties:
data:
items:
$ref: '#/definitions/api.UserPayload'
type: array
type: object
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
security:
- BearerAuth: []
summary: 用户列表
tags:
- 群组
securityDefinitions:
BearerAuth:
in: header
name: Authorization
type: apiKey
token:
in: header
name: api-token
type: apiKey
swagger: "2.0"

BIN
docs/api_swag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
docs/pc_ab.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
docs/pc_gr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
docs/web_admin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
docs/web_admin_gr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
docs/web_resetpwd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
docs/web_user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
docs/webclient_conf.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

5
generate_api.go Normal file
View File

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

33
global/global.go Normal file
View File

@@ -0,0 +1,33 @@
package global
import (
"Gwen/config"
"Gwen/lib/cache"
"Gwen/lib/jwt"
"Gwen/lib/lock"
"Gwen/lib/upload"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
"github.com/go-redis/redis/v8"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"gorm.io/gorm"
)
var (
DB *gorm.DB
Logger *logrus.Logger
Config config.Config
Viper *viper.Viper
Redis *redis.Client
Cache cache.Handler
Validator struct {
Validate *validator.Validate
VTrans ut.Translator
ValidStruct func(interface{}) []string
ValidVar func(field interface{}, tag string) []string
}
Oss *upload.Oss
Jwt *jwt.Jwt
Lock lock.Locker
)

73
go.mod Normal file
View File

@@ -0,0 +1,73 @@
module Gwen
go 1.22
require (
github.com/antonfisher/nested-logrus-formatter v1.3.1
github.com/fsnotify/fsnotify v1.5.1
github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6
github.com/gin-gonic/gin v1.9.0
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.11.2
github.com/go-redis/redis/v8 v8.11.4
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/sirupsen/logrus v1.8.1
github.com/spf13/viper v1.9.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.3
gorm.io/driver/mysql v1.5.7
gorm.io/driver/sqlite v1.5.6
gorm.io/gorm v1.25.7
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/bytedance/sonic v1.8.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-sqlite3 v1.14.23 // indirect
github.com/mitchellh/mapstructure v1.4.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/tools v0.7.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.63.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -0,0 +1,191 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/service"
_ "encoding/json"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"strconv"
)
type AddressBook struct {
}
// Detail 地址簿
// @Tags 地址簿
// @Summary 地址簿详情
// @Description 地址簿详情
// @Accept json
// @Produce json
// @Param id path int true "ID"
// @Success 200 {object} response.Response{data=model.AddressBook}
// @Failure 500 {object} response.Response
// @Router /admin/address_book/detail/{id} [get]
// @Security token
func (ct *AddressBook) Detail(c *gin.Context) {
id := c.Param("id")
iid, _ := strconv.Atoi(id)
t := service.AllService.AddressBookService.InfoByRowId(uint(iid))
u := service.AllService.UserService.CurUser(c)
if !service.AllService.UserService.IsAdmin(u) && t.UserId != u.Id {
response.Fail(c, 101, "无权限")
return
}
if t.RowId > 0 {
response.Success(c, t)
return
}
response.Fail(c, 101, "信息不存在")
return
}
// Create 创建地址簿
// @Tags 地址簿
// @Summary 创建地址簿
// @Description 创建地址簿
// @Accept json
// @Produce json
// @Param body body admin.AddressBookForm true "地址簿信息"
// @Success 200 {object} response.Response{data=model.AddressBook}
// @Failure 500 {object} response.Response
// @Router /admin/address_book/create [post]
// @Security token
func (ct *AddressBook) Create(c *gin.Context) {
f := &admin.AddressBookForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, "参数错误")
return
}
errList := global.Validator.ValidStruct(f)
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
t := f.ToAddressBook()
u := service.AllService.UserService.CurUser(c)
if !service.AllService.UserService.IsAdmin(u) || t.UserId == 0 {
t.UserId = u.Id
}
err := service.AllService.AddressBookService.Create(t)
if err != nil {
response.Fail(c, 101, "创建失败")
return
}
response.Success(c, u)
}
// 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"
// @Param is_my query int false "是否是我的"
// @Success 200 {object} response.Response{data=model.AddressBookList}
// @Failure 500 {object} response.Response
// @Router /admin/address_book/list [get]
// @Security token
func (ct *AddressBook) List(c *gin.Context) {
query := &admin.AddressBookQuery{}
if err := c.ShouldBindQuery(query); err != nil {
response.Fail(c, 101, "参数错误")
return
}
u := service.AllService.UserService.CurUser(c)
if !service.AllService.UserService.IsAdmin(u) || query.IsMy == 1 {
query.UserId = int(u.Id)
}
res := service.AllService.AddressBookService.List(query.Page, query.PageSize, func(tx *gorm.DB) {
if query.UserId > 0 {
tx.Where("user_id = ?", query.UserId)
}
})
response.Success(c, res)
}
// Update 编辑
// @Tags 地址簿
// @Summary 地址簿编辑
// @Description 地址簿编辑
// @Accept json
// @Produce json
// @Param body body admin.AddressBookForm true "地址簿信息"
// @Success 200 {object} response.Response{data=model.AddressBook}
// @Failure 500 {object} response.Response
// @Router /admin/address_book/update [post]
// @Security token
func (ct *AddressBook) Update(c *gin.Context) {
f := &admin.AddressBookForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, "参数错误")
return
}
errList := global.Validator.ValidStruct(f)
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
if f.RowId == 0 {
response.Fail(c, 101, "参数错误")
return
}
t := f.ToAddressBook()
u := service.AllService.UserService.CurUser(c)
if !service.AllService.UserService.IsAdmin(u) && t.UserId != u.Id {
response.Fail(c, 101, "无权限")
return
}
err := service.AllService.AddressBookService.Update(t)
if err != nil {
response.Fail(c, 101, "更新失败")
return
}
response.Success(c, nil)
}
// Delete 删除
// @Tags 地址簿
// @Summary 地址簿删除
// @Description 地址簿删除
// @Accept json
// @Produce json
// @Param body body admin.AddressBookForm true "地址簿信息"
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/address_book/delete [post]
// @Security token
func (ct *AddressBook) Delete(c *gin.Context) {
f := &admin.AddressBookForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, "系统错误")
return
}
id := f.RowId
errList := global.Validator.ValidVar(id, "required,gt=0")
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
t := service.AllService.AddressBookService.InfoByRowId(f.RowId)
u := service.AllService.UserService.CurUser(c)
if !service.AllService.UserService.IsAdmin(u) && t.UserId != u.Id {
response.Fail(c, 101, "无权限")
return
}
if u.Id > 0 {
err := service.AllService.AddressBookService.Delete(t)
if err == nil {
response.Success(c, nil)
return
}
response.Fail(c, 101, err.Error())
return
}
response.Fail(c, 101, "信息不存在")
}

View File

@@ -0,0 +1,83 @@
package admin
import (
"Gwen/global"
"Gwen/http/response"
"Gwen/lib/upload"
"fmt"
"github.com/gin-gonic/gin"
"os"
"time"
)
type File struct {
}
// OssToken 文件
// @Tags 文件
// @Summary 获取ossToken
// @Description 获取ossToken
// @Accept json
// @Produce json
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/file/oss_token [get]
// @Security token
func (f *File) OssToken(c *gin.Context) {
token := global.Oss.GetPolicyToken("")
response.Success(c, token)
}
type FileBack struct {
upload.CallbackBaseForm
Url string `json:"url"`
}
// Notify 上传成功后回调
func (f *File) Notify(c *gin.Context) {
res := global.Oss.Verify(c.Request)
if !res {
response.Fail(c, 101, "权限错误")
return
}
fm := &FileBack{}
if err := c.ShouldBind(fm); err != nil {
fmt.Println(err)
}
fm.Url = global.Config.Oss.Host + "/" + fm.Filename
response.Success(c, fm)
}
// Upload 上传文件到本地
// @Tags 文件
// @Summary 上传文件到本地
// @Description 上传文件到本地
// @Accept multipart/form-data
// @Produce json
// @Param file formData file true "上传文件示例"
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/file/upload [post]
// @Security token
func (f *File) Upload(c *gin.Context) {
file, _ := c.FormFile("file")
timePath := time.Now().Format("20060102") + "/"
webPath := "/upload/" + timePath
path := global.Config.Gin.ResourcesPath + webPath
dst := path + file.Filename
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
return
}
// 上传文件至指定目录
err = c.SaveUploadedFile(file, dst)
if err != nil {
return
}
// 返回文件web地址
response.Success(c, gin.H{
"url": webPath + file.Filename,
})
}

View File

@@ -0,0 +1,160 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/service"
"github.com/gin-gonic/gin"
"strconv"
)
type Group struct {
}
// Detail 群组
// @Tags 群组
// @Summary 群组详情
// @Description 群组详情
// @Accept json
// @Produce json
// @Param id path int true "ID"
// @Success 200 {object} response.Response{data=model.Group}
// @Failure 500 {object} response.Response
// @Router /admin/group/detail/{id} [get]
// @Security token
func (ct *Group) Detail(c *gin.Context) {
id := c.Param("id")
iid, _ := strconv.Atoi(id)
u := service.AllService.GroupService.InfoById(uint(iid))
if u.Id > 0 {
response.Success(c, u)
return
}
response.Fail(c, 101, "信息不存在")
return
}
// Create 创建群组
// @Tags 群组
// @Summary 创建群组
// @Description 创建群组
// @Accept json
// @Produce json
// @Param body body admin.GroupForm true "群组信息"
// @Success 200 {object} response.Response{data=model.Group}
// @Failure 500 {object} response.Response
// @Router /admin/group/create [post]
// @Security token
func (ct *Group) Create(c *gin.Context) {
f := &admin.GroupForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, "参数错误")
return
}
errList := global.Validator.ValidStruct(f)
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
u := f.ToGroup()
err := service.AllService.GroupService.Create(u)
if err != nil {
response.Fail(c, 101, "创建失败")
return
}
response.Success(c, u)
}
// List 列表
// @Tags 群组
// @Summary 群组列表
// @Description 群组列表
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param page_size query int false "页大小"
// @Success 200 {object} response.Response{data=model.GroupList}
// @Failure 500 {object} response.Response
// @Router /admin/group/list [get]
// @Security token
func (ct *Group) List(c *gin.Context) {
query := &admin.PageQuery{}
if err := c.ShouldBindQuery(query); err != nil {
response.Fail(c, 101, "参数错误")
return
}
res := service.AllService.GroupService.List(query.Page, query.PageSize, nil)
response.Success(c, res)
}
// Update 编辑
// @Tags 群组
// @Summary 群组编辑
// @Description 群组编辑
// @Accept json
// @Produce json
// @Param body body admin.GroupForm true "群组信息"
// @Success 200 {object} response.Response{data=model.Group}
// @Failure 500 {object} response.Response
// @Router /admin/group/update [post]
// @Security token
func (ct *Group) Update(c *gin.Context) {
f := &admin.GroupForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, "参数错误")
return
}
if f.Id == 0 {
response.Fail(c, 101, "参数错误")
return
}
errList := global.Validator.ValidStruct(f)
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
u := f.ToGroup()
err := service.AllService.GroupService.Update(u)
if err != nil {
response.Fail(c, 101, "更新失败")
return
}
response.Success(c, nil)
}
// Delete 删除
// @Tags 群组
// @Summary 群组删除
// @Description 群组删除
// @Accept json
// @Produce json
// @Param body body admin.GroupForm true "群组信息"
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/group/delete [post]
// @Security token
func (ct *Group) Delete(c *gin.Context) {
f := &admin.GroupForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, "系统错误")
return
}
id := f.Id
errList := global.Validator.ValidVar(id, "required,gt=0")
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
u := service.AllService.GroupService.InfoById(f.Id)
if u.Id > 0 {
err := service.AllService.GroupService.Delete(u)
if err == nil {
response.Success(c, nil)
return
}
response.Fail(c, 101, err.Error())
return
}
response.Fail(c, 101, "信息不存在")
}

View File

@@ -0,0 +1,74 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
adResp "Gwen/http/response/admin"
"Gwen/service"
"fmt"
"github.com/gin-gonic/gin"
)
type Login struct {
}
// Login 登录
// @Tags 登录
// @Summary 登录
// @Description 登录
// @Accept json
// @Produce json
// @Param body body admin.Login true "登录信息"
// @Success 200 {object} response.Response{data=adResp.LoginPayload}
// @Failure 500 {object} response.Response
// @Router /admin/login [post]
// @Security token
func (ct *Login) Login(c *gin.Context) {
fmt.Println("login")
f := &admin.Login{}
err := c.ShouldBindJSON(f)
if err != nil {
response.Fail(c, 101, "参数错误")
return
}
errList := global.Validator.ValidStruct(f)
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
u := service.AllService.UserService.InfoByUsernamePassword(f.Username, f.Password)
if u.Id == 0 {
response.Fail(c, 101, "用户名或密码错误")
return
}
ut := service.AllService.UserService.Login(u)
response.Success(c, &adResp.LoginPayload{
Token: ut.Token,
Username: u.Username,
RouteNames: service.AllService.UserService.RouteNames(u),
Nickname: u.Nickname,
})
}
// Logout 登出
// @Tags 登录
// @Summary 登出
// @Description 登出
// @Accept json
// @Produce json
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/logout [post]
func (ct *Login) Logout(c *gin.Context) {
u := service.AllService.UserService.CurUser(c)
token, ok := c.Get("token")
if ok {
service.AllService.UserService.Logout(u, token.(string))
}
response.Success(c, nil)
}

View File

@@ -0,0 +1,160 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/service"
"github.com/gin-gonic/gin"
"strconv"
)
type Peer struct {
}
// Detail 机器
// @Tags 机器
// @Summary 机器详情
// @Description 机器详情
// @Accept json
// @Produce json
// @Param id path int true "ID"
// @Success 200 {object} response.Response{data=model.Peer}
// @Failure 500 {object} response.Response
// @Router /admin/peer/detail/{id} [get]
// @Security token
func (ct *Peer) Detail(c *gin.Context) {
id := c.Param("id")
iid, _ := strconv.Atoi(id)
u := service.AllService.PeerService.InfoByRowId(uint(iid))
if u.RowId > 0 {
response.Success(c, u)
return
}
response.Fail(c, 101, "信息不存在")
return
}
// Create 创建机器
// @Tags 机器
// @Summary 创建机器
// @Description 创建机器
// @Accept json
// @Produce json
// @Param body body admin.PeerForm true "机器信息"
// @Success 200 {object} response.Response{data=model.Peer}
// @Failure 500 {object} response.Response
// @Router /admin/peer/create [post]
// @Security token
func (ct *Peer) Create(c *gin.Context) {
f := &admin.PeerForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, "参数错误")
return
}
errList := global.Validator.ValidStruct(f)
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
u := f.ToPeer()
err := service.AllService.PeerService.Create(u)
if err != nil {
response.Fail(c, 101, "创建失败")
return
}
response.Success(c, u)
}
// List 列表
// @Tags 机器
// @Summary 机器列表
// @Description 机器列表
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param page_size query int false "页大小"
// @Success 200 {object} response.Response{data=model.PeerList}
// @Failure 500 {object} response.Response
// @Router /admin/peer/list [get]
// @Security token
func (ct *Peer) List(c *gin.Context) {
query := &admin.PageQuery{}
if err := c.ShouldBindQuery(query); err != nil {
response.Fail(c, 101, "参数错误")
return
}
res := service.AllService.PeerService.List(query.Page, query.PageSize, nil)
response.Success(c, res)
}
// Update 编辑
// @Tags 机器
// @Summary 机器编辑
// @Description 机器编辑
// @Accept json
// @Produce json
// @Param body body admin.PeerForm true "机器信息"
// @Success 200 {object} response.Response{data=model.Peer}
// @Failure 500 {object} response.Response
// @Router /admin/peer/update [post]
// @Security token
func (ct *Peer) Update(c *gin.Context) {
f := &admin.PeerForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, "参数错误")
return
}
if f.RowId == 0 {
response.Fail(c, 101, "参数错误")
return
}
errList := global.Validator.ValidStruct(f)
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
u := f.ToPeer()
err := service.AllService.PeerService.Update(u)
if err != nil {
response.Fail(c, 101, "更新失败")
return
}
response.Success(c, nil)
}
// Delete 删除
// @Tags 机器
// @Summary 机器删除
// @Description 机器删除
// @Accept json
// @Produce json
// @Param body body admin.PeerForm true "机器信息"
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/peer/delete [post]
// @Security token
func (ct *Peer) Delete(c *gin.Context) {
f := &admin.PeerForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, "系统错误")
return
}
id := f.RowId
errList := global.Validator.ValidVar(id, "required,gt=0")
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
u := service.AllService.PeerService.InfoByRowId(f.RowId)
if u.RowId > 0 {
err := service.AllService.PeerService.Delete(u)
if err == nil {
response.Success(c, nil)
return
}
response.Fail(c, 101, err.Error())
return
}
response.Fail(c, 101, "信息不存在")
}

View File

@@ -0,0 +1,30 @@
package admin
import (
"Gwen/global"
"Gwen/http/response"
"github.com/gin-gonic/gin"
)
type Rustdesk struct {
}
// ServerConfig 服务配置
// @Tags ADMIN
// @Summary 服务配置
// @Description 服务配置,给webclient提供api-server
// @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)
}

View File

@@ -0,0 +1,190 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"strconv"
)
type Tag struct {
}
// Detail 标签
// @Tags 标签
// @Summary 标签详情
// @Description 标签详情
// @Accept json
// @Produce json
// @Param id path int true "ID"
// @Success 200 {object} response.Response{data=model.Tag}
// @Failure 500 {object} response.Response
// @Router /admin/tag/detail/{id} [get]
// @Security token
func (ct *Tag) Detail(c *gin.Context) {
id := c.Param("id")
iid, _ := strconv.Atoi(id)
t := service.AllService.TagService.InfoById(uint(iid))
u := service.AllService.UserService.CurUser(c)
if !service.AllService.UserService.IsAdmin(u) && t.UserId != u.Id {
response.Fail(c, 101, "无权限")
return
}
if t.Id > 0 {
response.Success(c, t)
return
}
response.Fail(c, 101, "信息不存在")
return
}
// Create 创建标签
// @Tags 标签
// @Summary 创建标签
// @Description 创建标签
// @Accept json
// @Produce json
// @Param body body admin.TagForm true "标签信息"
// @Success 200 {object} response.Response{data=model.Tag}
// @Failure 500 {object} response.Response
// @Router /admin/tag/create [post]
// @Security token
func (ct *Tag) Create(c *gin.Context) {
f := &admin.TagForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, "参数错误")
return
}
errList := global.Validator.ValidStruct(f)
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
t := f.ToTag()
u := service.AllService.UserService.CurUser(c)
if !service.AllService.UserService.IsAdmin(u) {
t.UserId = u.Id
}
err := service.AllService.TagService.Create(t)
if err != nil {
response.Fail(c, 101, "创建失败")
return
}
response.Success(c, u)
}
// List 列表
// @Tags 标签
// @Summary 标签列表
// @Description 标签列表
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param page_size query int false "页大小"
// @Param is_my query int false "是否是我的"
// @Param user_id query int false "用户id"
// @Success 200 {object} response.Response{data=model.TagList}
// @Failure 500 {object} response.Response
// @Router /admin/tag/list [get]
// @Security token
func (ct *Tag) List(c *gin.Context) {
query := &admin.TagQuery{}
if err := c.ShouldBindQuery(query); err != nil {
response.Fail(c, 101, "参数错误")
return
}
u := service.AllService.UserService.CurUser(c)
if !service.AllService.UserService.IsAdmin(u) || query.IsMy == 1 {
query.UserId = int(u.Id)
}
res := service.AllService.TagService.List(query.Page, query.PageSize, func(tx *gorm.DB) {
if query.UserId > 0 {
tx.Where("user_id = ?", query.UserId)
}
})
response.Success(c, res)
}
// Update 编辑
// @Tags 标签
// @Summary 标签编辑
// @Description 标签编辑
// @Accept json
// @Produce json
// @Param body body admin.TagForm true "标签信息"
// @Success 200 {object} response.Response{data=model.Tag}
// @Failure 500 {object} response.Response
// @Router /admin/tag/update [post]
// @Security token
func (ct *Tag) Update(c *gin.Context) {
f := &admin.TagForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, "参数错误")
return
}
errList := global.Validator.ValidStruct(f)
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
if f.Id == 0 {
response.Fail(c, 101, "参数错误")
return
}
t := f.ToTag()
u := service.AllService.UserService.CurUser(c)
if !service.AllService.UserService.IsAdmin(u) && t.UserId != u.Id {
response.Fail(c, 101, "无权限")
return
}
err := service.AllService.TagService.Update(t)
if err != nil {
response.Fail(c, 101, "更新失败")
return
}
response.Success(c, nil)
}
// Delete 删除
// @Tags 标签
// @Summary 标签删除
// @Description 标签删除
// @Accept json
// @Produce json
// @Param body body admin.TagForm true "标签信息"
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/tag/delete [post]
// @Security token
func (ct *Tag) Delete(c *gin.Context) {
f := &admin.TagForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, "系统错误")
return
}
id := f.Id
errList := global.Validator.ValidVar(id, "required,gt=0")
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
t := service.AllService.TagService.InfoById(f.Id)
u := service.AllService.UserService.CurUser(c)
if !service.AllService.UserService.IsAdmin(u) && t.UserId != u.Id {
response.Fail(c, 101, "无权限")
return
}
if u.Id > 0 {
err := service.AllService.TagService.Delete(t)
if err == nil {
response.Success(c, nil)
return
}
response.Fail(c, 101, err.Error())
return
}
response.Fail(c, 101, "信息不存在")
}

View File

@@ -0,0 +1,261 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
adResp "Gwen/http/response/admin"
"Gwen/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"strconv"
)
type User struct {
}
// Detail 管理员
// @Tags 用户
// @Summary 管理员详情
// @Description 管理员详情
// @Accept json
// @Produce json
// @Param id path int true "ID"
// @Success 200 {object} response.Response{data=model.User}
// @Failure 500 {object} response.Response
// @Router /admin/user/detail/{id} [get]
// @Security token
func (ct *User) Detail(c *gin.Context) {
id := c.Param("id")
iid, _ := strconv.Atoi(id)
u := service.AllService.UserService.InfoById(uint(iid))
if u.Id > 0 {
response.Success(c, u)
return
}
response.Fail(c, 101, "信息不存在")
return
}
// Create 管理员
// @Tags 用户
// @Summary 创建管理员
// @Description 创建管理员
// @Accept json
// @Produce json
// @Param body body admin.UserForm true "管理员信息"
// @Success 200 {object} response.Response{data=model.User}
// @Failure 500 {object} response.Response
// @Router /admin/user/create [post]
// @Security token
func (ct *User) Create(c *gin.Context) {
f := &admin.UserForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, "参数错误")
return
}
errList := global.Validator.ValidStruct(f)
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
u := f.ToUser()
err := service.AllService.UserService.Create(u)
if err != nil {
response.Fail(c, 101, "创建失败")
return
}
response.Success(c, u)
}
// List 列表
// @Tags 用户
// @Summary 管理员列表
// @Description 管理员列表
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param page_size query int false "页大小"
// @Param username query int false "账户"
// @Success 200 {object} response.Response{data=model.UserList}
// @Failure 500 {object} response.Response
// @Router /admin/user/list [get]
// @Security token
func (ct *User) List(c *gin.Context) {
query := &admin.UserQuery{}
if err := c.ShouldBindQuery(query); err != nil {
response.Fail(c, 101, "参数错误")
return
}
res := service.AllService.UserService.List(query.Page, query.PageSize, func(tx *gorm.DB) {
if query.Username != "" {
tx.Where("username like ?", "%"+query.Username+"%")
}
})
response.Success(c, res)
}
// Update 编辑
// @Tags 用户
// @Summary 管理员编辑
// @Description 管理员编辑
// @Accept json
// @Produce json
// @Param body body admin.UserForm true "用户信息"
// @Success 200 {object} response.Response{data=model.User}
// @Failure 500 {object} response.Response
// @Router /admin/user/update [post]
// @Security token
func (ct *User) Update(c *gin.Context) {
f := &admin.UserForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, "参数错误:"+err.Error())
return
}
if f.Id == 0 {
response.Fail(c, 101, "参数错误")
return
}
errList := global.Validator.ValidStruct(f)
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
u := f.ToUser()
err := service.AllService.UserService.Update(u)
if err != nil {
response.Fail(c, 101, "更新失败")
return
}
response.Success(c, nil)
}
// Delete 删除
// @Tags 用户
// @Summary 管理员删除
// @Description 管理员编删除
// @Accept json
// @Produce json
// @Param body body admin.UserForm true "用户信息"
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/user/delete [post]
// @Security token
func (ct *User) Delete(c *gin.Context) {
f := &admin.UserForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, "系统错误")
return
}
id := f.Id
errList := global.Validator.ValidVar(id, "required,gt=0")
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
u := service.AllService.UserService.InfoById(f.Id)
if u.Id > 0 {
err := service.AllService.UserService.Delete(u)
if err == nil {
response.Success(c, nil)
return
}
response.Fail(c, 101, err.Error())
return
}
response.Fail(c, 101, "信息不存在")
}
// UpdatePassword 修改密码
// @Tags 用户
// @Summary 修改密码
// @Description 修改密码
// @Accept json
// @Produce json
// @Param body body admin.UserPasswordForm true "用户信息"
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/user/updatePassword [post]
// @Security token
func (ct *User) UpdatePassword(c *gin.Context) {
f := &admin.UserPasswordForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, "参数错误")
return
}
errList := global.Validator.ValidStruct(f)
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
u := service.AllService.UserService.InfoById(f.Id)
if u.Id == 0 {
response.Fail(c, 101, "信息不存在")
return
}
err := service.AllService.UserService.UpdatePassword(u, f.Password)
if err != nil {
response.Fail(c, 101, "更新失败")
return
}
response.Success(c, nil)
}
// Current 当前用户
// @Tags 用户
// @Summary 当前用户
// @Description 当前用户
// @Accept json
// @Produce json
// @Success 200 {object} response.Response{data=adResp.LoginPayload}
// @Failure 500 {object} response.Response
// @Router /admin/user/current [get]
// @Security token
func (ct *User) Current(c *gin.Context) {
u := service.AllService.UserService.CurUser(c)
token, _ := c.Get("token")
t := token.(string)
response.Success(c, &adResp.LoginPayload{
Token: t,
Username: u.Username,
RouteNames: service.AllService.UserService.RouteNames(u),
Nickname: u.Nickname,
})
}
// ChangeCurPwd 修改当前用户密码
// @Tags 用户
// @Summary 修改当前用户密码
// @Description 修改当前用户密码
// @Accept json
// @Produce json
// @Param body body admin.ChangeCurPasswordForm true "用户信息"
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/user/changeCurPwd [post]
// @Security token
func (ct *User) ChangeCurPwd(c *gin.Context) {
f := &admin.ChangeCurPasswordForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, "参数错误")
return
}
errList := global.Validator.ValidStruct(f)
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
u := service.AllService.UserService.CurUser(c)
oldPwd := service.AllService.UserService.EncryptPassword(f.OldPassword)
if u.Password != oldPwd {
response.Fail(c, 101, "旧密码错误")
return
}
err := service.AllService.UserService.UpdatePassword(u, f.NewPassword)
if err != nil {
response.Fail(c, 101, "更新失败")
return
}
response.Success(c, nil)
}

150
http/controller/api/ab.go Normal file
View File

@@ -0,0 +1,150 @@
package api
import (
requstform "Gwen/http/request/api"
"Gwen/http/response"
"Gwen/http/response/api"
"Gwen/model"
"Gwen/service"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
type Ab struct {
}
// Ab
// @Tags 地址
// @Summary 地址列表
// @Description 地址列表
// @Accept json
// @Produce json
// @Success 200 {object} response.Response
// @Failure 500 {object} response.ErrorResponse
// @Router /ab [get]
// @Security BearerAuth
func (a *Ab) Ab(c *gin.Context) {
user := service.AllService.UserService.CurUser(c)
al := service.AllService.AddressBookService.ListByUserId(user.Id, 1, 1000)
tags := service.AllService.TagService.ListByUserId(user.Id)
tagColors := map[string]uint{}
//将tags中的name转成一个以逗号分割的字符串
var tagNames []string
for _, tag := range tags.Tags {
tagNames = append(tagNames, tag.Name)
tagColors[tag.Name] = tag.Color
}
tgc, _ := json.Marshal(tagColors)
res := &api.AbList{
Peers: al.AddressBooks,
Tags: tagNames,
TagColors: string(tgc),
}
data, _ := json.Marshal(res)
c.JSON(http.StatusOK, gin.H{
"data": string(data),
//"licensed_devices": 999,
})
}
// UpAb
// @Tags 地址
// @Summary 地址更新
// @Description 地址更新
// @Accept json
// @Produce json
// @Param body body requstform.AddressBookForm true "地址表单"
// @Success 200 {string} string "null"
// @Failure 500 {object} response.ErrorResponse
// @Router /ab [post]
// @Security BearerAuth
func (a *Ab) UpAb(c *gin.Context) {
abf := &requstform.AddressBookForm{}
err := c.ShouldBindJSON(&abf)
if err != nil {
fmt.Println(err)
response.Error(c, "参数错误")
return
}
abd := &requstform.AddressBookFormData{}
err = json.Unmarshal([]byte(abf.Data), abd)
if err != nil {
response.Error(c, "系统错误")
return
}
//fmt.Println(abd)
//for _, peer := range abd.Peers {
// fmt.Println(peer)
//}
user := service.AllService.UserService.CurUser(c)
err = service.AllService.AddressBookService.UpdateAddressBook(abd.Peers, user.Id)
if err != nil {
c.Abort()
return
}
tc := map[string]uint{}
err = json.Unmarshal([]byte(abd.TagColors), &tc)
if err != nil {
fmt.Println(err)
response.Error(c, "系统错误")
return
} else {
service.AllService.TagService.UpdateTags(user.Id, tc)
}
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)
}
// TagAdd
// @Tags 地址
// @Summary 标签添加
// @Description 标签
// @Accept json
// @Produce json
// @Success 200 {string} string
// @Failure 500 {object} response.ErrorResponse
// @Router /ab/add [post]
// @Security BearerAuth
func (a *Ab) TagAdd(c *gin.Context) {
t := &model.Tag{}
err := c.ShouldBindJSON(t)
if err != nil {
fmt.Println(err)
response.Error(c, "参数错误")
return
}
//u := service.AllService.UserService.CurUser(c)
//err = service.AllService.TagService.UpdateTags(t.Name, t.Color, user.Id)
//if err != nil {
// response.Error(c, "操作失败")
// return
//}
c.JSON(http.StatusOK, "")
}

View File

@@ -0,0 +1,115 @@
package api
import (
apiReq "Gwen/http/request/api"
"Gwen/http/response"
apiResp "Gwen/http/response/api"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin"
"net/http"
)
type Group struct {
}
// Users 用户列表
// @Tags 群组
// @Summary 用户列表
// @Description 用户列表
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param pageSize query int false "每页数量"
// @Param status query int false "状态"
// @Param accessible query string false "accessible"
// @Success 200 {object} response.DataResponse{data=[]apiResp.UserPayload}
// @Failure 500 {object} response.ErrorResponse
// @Router /users [get]
// @Security BearerAuth
func (g *Group) Users(c *gin.Context) {
u := service.AllService.UserService.CurUser(c)
if !*u.IsAdmin {
gr := service.AllService.GroupService.InfoById(u.GroupId)
if gr.Type != model.GroupTypeShare {
response.Error(c, "不是管理员也不在分享组")
return
}
}
q := &apiReq.UserListQuery{}
err := c.ShouldBindQuery(&q)
if err != nil {
response.Error(c, err.Error())
return
}
userList := service.AllService.UserService.ListByGroupId(u.GroupId, q.Page, q.PageSize)
var data []*apiResp.UserPayload
for _, user := range userList.Users {
up := &apiResp.UserPayload{}
up.FromUser(user)
data = append(data, up)
}
c.JSON(http.StatusOK, response.DataResponse{
Total: uint(userList.Total),
Data: data,
})
}
// Peers
// @Tags 群组
// @Summary 机器
// @Description 机器
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param pageSize query int false "每页数量"
// @Param status query int false "状态"
// @Param accessible query string false "accessible"
// @Success 200 {object} response.DataResponse
// @Failure 500 {object} response.Response
// @Router /peers [get]
// @Security BearerAuth
func (g *Group) Peers(c *gin.Context) {
u := service.AllService.UserService.CurUser(c)
if !*u.IsAdmin {
gr := service.AllService.GroupService.InfoById(u.GroupId)
if gr.Type != model.GroupTypeShare {
response.Error(c, "不是管理员也不在分享组")
return
}
}
q := &apiReq.PeerListQuery{}
err := c.ShouldBindQuery(&q)
if err != nil {
response.Error(c, err.Error())
return
}
users := service.AllService.UserService.ListIdAndNameByGroupId(u.GroupId)
namesById := make(map[uint]string)
userIds := make([]uint, 0)
for _, user := range users {
namesById[user.Id] = user.Username
userIds = append(userIds, user.Id)
}
peerList := service.AllService.AddressBookService.ListByUserIds(userIds, q.Page, q.PageSize)
var data []*apiResp.GroupPeerPayload
for _, ab := range peerList.AddressBooks {
uname, ok := namesById[ab.UserId]
if !ok {
uname = ""
}
pp := &apiResp.GroupPeerPayload{}
pp.FromAddressBook(ab, uname)
data = append(data, pp)
}
c.JSON(http.StatusOK, response.DataResponse{
Total: uint(peerList.Total),
Data: data,
})
}

View File

@@ -0,0 +1,39 @@
package api
import (
"Gwen/http/response"
"github.com/gin-gonic/gin"
"net/http"
)
type Index struct {
}
// Index 首页
// @Tags 首页
// @Summary 首页
// @Description 首页
// @Accept json
// @Produce json
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router / [get]
func (i *Index) Index(c *gin.Context) {
response.Success(
c,
"Hello Gwen",
)
}
// Heartbeat 心跳
// @Tags 首页
// @Summary 心跳
// @Description 心跳
// @Accept json
// @Produce json
// @Success 200 {object} nil
// @Failure 500 {object} response.Response
// @Router /heartbeat [post]
func (i *Index) Heartbeat(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{})
}

View File

@@ -0,0 +1,90 @@
package api
import (
"Gwen/global"
"Gwen/http/request/api"
"Gwen/http/response"
apiResp "Gwen/http/response/api"
"Gwen/service"
"github.com/gin-gonic/gin"
"net/http"
)
type Login struct {
}
// Login 登录
// @Tags 登录
// @Summary 登录
// @Description 登录
// @Accept json
// @Produce json
// @Param body body api.LoginForm true "登录表单"
// @Success 200 {object} apiResp.LoginRes
// @Failure 500 {object} response.ErrorResponse
// @Router /login [post]
func (l *Login) Login(c *gin.Context) {
f := &api.LoginForm{}
err := c.ShouldBindJSON(f)
if err != nil {
response.Error(c, "系统错误")
return
}
errList := global.Validator.ValidStruct(f)
if len(errList) > 0 {
response.Error(c, errList[0])
return
}
u := service.AllService.UserService.InfoByUsernamePassword(f.Username, f.Password)
if u.Id == 0 {
response.Error(c, "用户名或密码错误")
return
}
ut := service.AllService.UserService.Login(u)
c.JSON(http.StatusOK, apiResp.LoginRes{
AccessToken: ut.Token,
Type: "access_token",
User: *(&apiResp.UserPayload{}).FromUser(u),
})
}
// LoginOptions
// @Tags 登录
// @Summary 登录选项
// @Description 登录选项
// @Accept json
// @Produce json
// @Success 200 {object} []string
// @Failure 500 {object} response.ErrorResponse
// @Router /login-options [post]
func (l *Login) LoginOptions(c *gin.Context) {
test := []string{
//"common-oidc/[{\"name\":\"google\"},{\"name\":\"github\"},{\"name\":\"facebook\"},{\"name\":\"网页授权登录\",\"icon\":\"\"}]",
//"oidc/myapp",
}
c.JSON(http.StatusOK, test)
}
// Logout
// @Tags 登录
// @Summary 登出
// @Description 登出
// @Accept json
// @Produce json
// @Success 200 {string} string
// @Failure 500 {object} response.ErrorResponse
// @Router /logout [post]
func (l *Login) Logout(c *gin.Context) {
u := service.AllService.UserService.CurUser(c)
token, ok := c.Get("token")
if ok {
service.AllService.UserService.Logout(u, token.(string))
}
c.JSON(http.StatusOK, nil)
}

View File

@@ -0,0 +1,48 @@
package api
import (
requstform "Gwen/http/request/api"
"Gwen/http/response"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"net/http"
)
type Peer struct {
}
// SysInfo
// @Tags 地址
// @Summary 提交系统信息
// @Description 提交系统信息
// @Accept json
// @Produce json
// @Param body body requstform.PeerForm true "系统信息表单"
// @Success 200 {string} string "SYSINFO_UPDATED,ID_NOT_FOUND"
// @Failure 500 {object} response.ErrorResponse
// @Router /sysinfo [post]
// @Security BearerAuth
func (p *Peer) SysInfo(c *gin.Context) {
f := &requstform.PeerForm{}
err := c.ShouldBindBodyWith(f, binding.JSON)
if err != nil {
response.Error(c, err.Error())
return
}
pe := service.AllService.PeerService.FindById(f.Id)
if pe == nil || pe.RowId == 0 {
pe = f.ToPeer()
err = service.AllService.PeerService.Create(pe)
if err != nil {
response.Error(c, err.Error())
return
}
}
//SYSINFO_UPDATED 上传成功
//ID_NOT_FOUND 下次心跳会上传
//直接响应文本
c.String(http.StatusOK, "")
}

View File

@@ -0,0 +1,74 @@
package api
import (
apiResp "Gwen/http/response/api"
"Gwen/service"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
type User struct {
}
// currentUser 当前用户
// @Tags 用户
// @Summary 用户信息
// @Description 用户信息
// @Accept json
// @Produce json
// @Success 200 {object} apiResp.UserPayload
// @Failure 500 {object} response.Response
// @Router /currentUser [get]
// @Security token
func (u *User) currentUser(c *gin.Context) {
user := service.AllService.UserService.CurUser(c)
up := (&apiResp.UserPayload{}).FromUser(user)
c.JSON(http.StatusOK, up)
}
// Info 用户信息
// @Tags 用户
// @Summary 用户信息
// @Description 用户信息
// @Accept json
// @Produce json
// @Success 200 {object} apiResp.UserPayload
// @Failure 500 {object} response.Response
// @Router /api [get]
// @Security token
func (u *User) Info(c *gin.Context) {
user := service.AllService.UserService.CurUser(c)
up := (&apiResp.UserPayload{}).FromUser(user)
c.JSON(http.StatusOK, up)
}
// Personal
// @Tags 用户
// @Summary 个人信息
// @Description 个人信息
// @Accept json
// @Produce json
// @Param string body string false "string valid"
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /ab/personal [post]
// @Security BearerAuth
func (u *User) Personal(c *gin.Context) {
//打印全部body
fmt.Println(c.Request.Body)
/**
guid = json['guid'] ?? '',
name = json['name'] ?? '',
owner = json['owner'] ?? '',
note = json['note'] ?? '',
rule = json['rule'] ?? 0;
*/
//如果返回了guid后面的请求会有变化
c.JSON(http.StatusOK, gin.H{
//"guid": "123456",
//"name": "admindddd",
//"rule": 1,
})
}

View File

@@ -0,0 +1,42 @@
package api
import (
"Gwen/global"
"Gwen/http/response"
"Gwen/http/response/api"
"Gwen/service"
"github.com/gin-gonic/gin"
)
type WebClient struct {
}
// ServerConfig 服务配置
// @Tags WEBCLIENT
// @Summary 服务配置
// @Description 服务配置,给webclient提供api-server
// @Accept json
// @Produce json
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /server-config [get]
// @Security token
func (i *WebClient) ServerConfig(c *gin.Context) {
u := service.AllService.UserService.CurUser(c)
peers := map[string]*api.WebClientPeerPayload{}
abs := service.AllService.AddressBookService.ListByUserId(u.Id, 1, 100)
for _, ab := range abs.AddressBooks {
pp := &api.WebClientPeerPayload{}
pp.FromAddressBook(ab)
peers[ab.Id] = pp
}
response.Success(
c,
gin.H{
"id_server": global.Config.Rustdesk.IdServer,
"key": global.Config.Rustdesk.Key,
//"peers": peers,
},
)
}

View File

@@ -0,0 +1,68 @@
package web
import (
"Gwen/global"
"github.com/gin-gonic/gin"
)
type Index struct {
}
func (i *Index) ConfigJs(c *gin.Context) {
apiServer := global.Config.Rustdesk.ApiServer
tmp := `
window._gwen = {}
window._gwen.kv = {}
function getQueryVariable() {
const query = window.location.hash.substring(3);
const vars = query.split("&");
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split("=");
window._gwen.kv[pair[0]] = pair[1]
}
}
getQueryVariable()
const id = window._gwen.kv.id || ''
if (id) {
localStorage.setItem('remote-id', id)
}
window._gwen.hosts = [
"rs-sg.rustdesk.com",
"rs-cn.rustdesk.com",
"rs-us.rustdesk.com",
]
localStorage.setItem('api-server', "` + apiServer + `")
const autoWriteServer = () => {
return setTimeout(() => {
const token = localStorage.getItem('access_token')
const apiserver = localStorage.getItem('api-server')
if (token && apiserver) {
fetch(apiserver + "/api/server-config", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
}
}
).then(res => res.json()).then(res => {
if (res.code === 0) {
if(!localStorage.getItem('custom-rendezvous-server') || !localStorage.getItem('key') ) {
localStorage.setItem('custom-rendezvous-server', res.data.id_server)
localStorage.setItem('key', res.data.key)
}
if (res.data.peers) {
localStorage.setItem('peers', JSON.stringify(res.data.peers))
}
}
})
} else {
autoWriteServer()
}
}, 1000)
}
autoWriteServer()
`
c.String(200, tmp)
}

30
http/http.go Normal file
View File

@@ -0,0 +1,30 @@
package http
import (
"Gwen/global"
"Gwen/http/middleware"
"Gwen/http/router"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"net/http"
)
func ApiInit() {
gin.SetMode(global.Config.Gin.Mode)
g := gin.New()
if global.Config.Gin.Mode == gin.ReleaseMode {
//修改gin Recovery日志 输出为logger的输出点
if global.Logger != nil {
gin.DefaultErrorWriter = global.Logger.WriterLevel(logrus.ErrorLevel)
}
}
g.NoRoute(func(c *gin.Context) {
c.String(http.StatusNotFound, "404 not found")
})
g.Use(middleware.Logger(), gin.Recovery())
router.WebInit(g)
router.Init(g)
router.ApiInit(g)
Run(g, global.Config.Gin.ApiAddr)
}

32
http/middleware/admin.go Normal file
View File

@@ -0,0 +1,32 @@
package middleware
import (
"Gwen/http/response"
"Gwen/service"
"github.com/gin-gonic/gin"
)
// AdminAuth 后台权限验证中间件
func AdminAuth() gin.HandlerFunc {
return func(c *gin.Context) {
//测试先关闭
token := c.GetHeader("api-token")
if token == "" {
response.Fail(c, 403, "请先登录")
c.Abort()
return
}
user := service.AllService.UserService.InfoByAccessToken(token)
if user.Id == 0 {
response.Fail(c, 403, "请先登录")
c.Abort()
return
}
c.Set("curUser", user)
c.Set("token", token)
c.Next()
}
}

View File

@@ -0,0 +1,22 @@
package middleware
import (
"Gwen/http/response"
"Gwen/service"
"github.com/gin-gonic/gin"
)
// AdminPrivilege ...
func AdminPrivilege() gin.HandlerFunc {
return func(c *gin.Context) {
u := service.AllService.UserService.CurUser(c)
if !service.AllService.UserService.IsAdmin(u) {
response.Fail(c, 403, "无权限")
c.Abort()
return
}
c.Next()
}
}

23
http/middleware/cors.go Normal file
View File

@@ -0,0 +1,23 @@
package middleware
import (
"github.com/gin-gonic/gin"
"net/http"
)
// Cors 跨域
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.GetHeader("Origin")
//fmt.Println("origin", origin)
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Headers", "api-token,content-type,authorization ")
c.Header("Access-Control-Allow-Methods", c.Request.Method)
c.Header("Access-Control-Allow-Credentials", "true")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}

50
http/middleware/jwt.go Normal file
View File

@@ -0,0 +1,50 @@
package middleware
import (
"Gwen/global"
"Gwen/http/response"
"Gwen/service"
"github.com/gin-gonic/gin"
)
func JwtAuth() gin.HandlerFunc {
return func(c *gin.Context) {
//测试先关闭
token := c.GetHeader("api-token")
if token == "" {
response.Fail(c, 403, "请先登录")
c.Abort()
return
}
uid, err := global.Jwt.ParseToken(token)
if err != nil {
response.Fail(c, 403, "请先登录")
c.Abort()
return
}
if uid == 0 {
response.Fail(c, 403, "请先登录")
c.Abort()
return
}
user := service.AllService.UserService.InfoById(uid)
//user := &model.User{
// Id: uid,
// Username: "测试用户",
//}
if user.Id == 0 {
response.Fail(c, 403, "请先登录")
c.Abort()
return
}
if !service.AllService.UserService.CheckUserEnable(user) {
response.Fail(c, 101, "你已被禁用")
c.Abort()
return
}
c.Set("curUser", user)
c.Next()
}
}

20
http/middleware/logger.go Normal file
View File

@@ -0,0 +1,20 @@
package middleware
import (
"Gwen/global"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
// Logger 日志中间件
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
global.Logger.WithFields(
logrus.Fields{
"uri": c.Request.URL,
"ip": c.ClientIP(),
"method": c.Request.Method,
}).Debug("Request")
c.Next()
}
}

View File

@@ -0,0 +1,44 @@
package middleware
import (
"Gwen/service"
"github.com/gin-gonic/gin"
)
func RustAuth() gin.HandlerFunc {
return func(c *gin.Context) {
//获取HTTP_AUTHORIZATION
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(401, gin.H{
"error": "Unauthorized",
})
c.Abort()
return
}
//提取token格式是Bearer {token}
//这里只是简单的提取
token = token[7:]
//验证token
user := service.AllService.UserService.InfoByAccessToken(token)
if user.Id == 0 {
c.JSON(401, gin.H{
"error": "Unauthorized",
})
c.Abort()
return
}
if !service.AllService.UserService.CheckUserEnable(user) {
c.JSON(401, gin.H{
"error": "账号已被禁用",
})
c.Abort()
return
}
c.Set("curUser", user)
c.Set("token", token)
c.Next()
}
}

View File

@@ -0,0 +1,56 @@
package admin
import (
"Gwen/model"
"encoding/json"
)
type AddressBookForm struct {
RowId uint `json:"row_id"`
Id string `json:"id" validate:"required"`
Username string `json:"username" `
Password string `json:"password" `
Hostname string `json:"hostname" `
Alias string `json:"alias" `
Platform string `json:"platform" `
Tags []string `json:"tags"`
Hash string `json:"hash"`
UserId uint `json:"user_id"`
ForceAlwaysRelay bool `json:"force_always_relay"`
RdpPort string `json:"rdp_port"`
RdpUsername string `json:"rdp_username"`
Online bool `json:"online"`
LoginName string `json:"login_name" `
SameServer bool `json:"same_server"`
}
func (a AddressBookForm) ToAddressBook() *model.AddressBook {
//tags转换
tags, _ := json.Marshal(a.Tags)
return &model.AddressBook{
RowId: a.RowId,
Id: a.Id,
Username: a.Username,
Password: a.Password,
Hostname: a.Hostname,
Alias: a.Alias,
Platform: a.Platform,
Tags: tags,
Hash: a.Hash,
UserId: a.UserId,
ForceAlwaysRelay: a.ForceAlwaysRelay,
RdpPort: a.RdpPort,
RdpUsername: a.RdpUsername,
Online: a.Online,
LoginName: a.LoginName,
SameServer: a.SameServer,
}
}
type AddressBookQuery struct {
UserId int `form:"user_id"`
IsMy int `form:"is_my"`
PageQuery
}

View File

@@ -0,0 +1,21 @@
package admin
import "Gwen/model"
type GroupForm struct {
Id uint `json:"id"`
Name string `json:"name" validate:"required"`
}
func (gf *GroupForm) FromGroup(group *model.Group) *GroupForm {
gf.Id = group.Id
gf.Name = group.Name
return gf
}
func (gf *GroupForm) ToGroup() *model.Group {
group := &model.Group{}
group.Id = gf.Id
group.Name = gf.Name
return group
}

View File

@@ -0,0 +1,6 @@
package admin
type Login struct {
Username string `json:"username" validate:"required" label:"用户名"`
Password string `json:"password,omitempty" validate:"required" label:"密码"`
}

View File

@@ -0,0 +1,30 @@
package admin
import "Gwen/model"
type PeerForm struct {
RowId uint `json:"row_id" `
Id string `json:"id"`
Cpu string `json:"cpu"`
Hostname string `json:"hostname"`
Memory string `json:"memory"`
Os string `json:"os"`
Username string `json:"username"`
Uuid string `json:"uuid"`
Version string `json:"version"`
}
// ToPeer
func (f *PeerForm) ToPeer() *model.Peer {
return &model.Peer{
RowId: f.RowId,
Id: f.Id,
Cpu: f.Cpu,
Hostname: f.Hostname,
Memory: f.Memory,
Os: f.Os,
Username: f.Username,
Uuid: f.Uuid,
Version: f.Version,
}
}

33
http/request/admin/tag.go Normal file
View File

@@ -0,0 +1,33 @@
package admin
import "Gwen/model"
type TagForm struct {
Id uint `json:"id"`
Name string `json:"name" validate:"required"`
Color uint `json:"color" validate:"required"`
UserId uint `json:"user_id" validate:"required"`
}
func (f *TagForm) FromTag(group *model.Tag) *TagForm {
f.Id = group.Id
f.Name = group.Name
f.Color = group.Color
f.UserId = group.UserId
return f
}
func (f *TagForm) ToTag() *model.Tag {
i := &model.Tag{}
i.Id = f.Id
i.Name = f.Name
i.Color = f.Color
i.UserId = f.UserId
return i
}
type TagQuery struct {
UserId int `form:"user_id"`
IsMy int `form:"is_my"`
PageQuery
}

View File

@@ -0,0 +1,57 @@
package admin
import (
"Gwen/model"
)
type UserForm struct {
Id uint `json:"id"`
Username string `json:"username" validate:"required,gte=4,lte=10"`
//Password string `json:"password" validate:"required,gte=4,lte=20"`
Nickname string `json:"nickname" validate:"required"`
Avatar string `json:"avatar"`
GroupId uint `json:"group_id" validate:"required"`
IsAdmin *bool `json:"is_admin" `
Status model.StatusCode `json:"status" validate:"required,gte=0"`
}
func (uf *UserForm) FromUser(user *model.User) *UserForm {
uf.Id = user.Id
uf.Username = user.Username
uf.Nickname = user.Nickname
uf.Avatar = user.Avatar
uf.GroupId = user.GroupId
uf.IsAdmin = user.IsAdmin
uf.Status = user.Status
return uf
}
func (uf *UserForm) ToUser() *model.User {
user := &model.User{}
user.Id = uf.Id
user.Username = uf.Username
user.Nickname = uf.Nickname
user.Avatar = uf.Avatar
user.GroupId = uf.GroupId
user.IsAdmin = uf.IsAdmin
user.Status = uf.Status
return user
}
type PageQuery struct {
Page uint `form:"page"`
PageSize uint `form:"page_size"`
}
type UserQuery struct {
PageQuery
Username string `form:"username"`
}
type UserPasswordForm struct {
Id uint `json:"id" validate:"required"`
Password string `json:"password" validate:"required,gte=4,lte=20"`
}
type ChangeCurPasswordForm struct {
OldPassword string `json:"old_password" validate:"required,gte=4,lte=20"`
NewPassword string `json:"new_password" validate:"required,gte=4,lte=20"`
}

37
http/request/api/peer.go Normal file
View File

@@ -0,0 +1,37 @@
package api
import "Gwen/model"
type AddressBookFormData struct {
Tags []string `json:"tags"`
Peers []*model.AddressBook `json:"peers"`
TagColors string `json:"tag_colors"`
}
type AddressBookForm struct {
Data string `json:"data" example:"{\"tags\":[\"tag1\",\"tag2\",\"tag3\"],\"peers\":[{\"id\":\"abc\",\"username\":\"abv-l\",\"hostname\":\"\",\"platform\":\"Windows\",\"alias\":\"\",\"tags\":[\"tag1\",\"tag2\"],\"hash\":\"hash\"}],\"tag_colors\":\"{\\\"tag1\\\":4288585374,\\\"tag2\\\":4278238420,\\\"tag3\\\":4291681337}\"}"`
}
type PeerForm struct {
Cpu string `json:"cpu"`
Hostname string `json:"hostname"`
Id string `json:"id"`
Memory string `json:"memory"`
Os string `json:"os"`
Username string `json:"username"`
Uuid string `json:"uuid"`
Version string `json:"version"`
}
func (pf *PeerForm) ToPeer() *model.Peer {
return &model.Peer{
Cpu: pf.Cpu,
Hostname: pf.Hostname,
Id: pf.Id,
Memory: pf.Memory,
Os: pf.Os,
Username: pf.Username,
Uuid: pf.Uuid,
Version: pf.Version,
}
}

41
http/request/api/user.go Normal file
View File

@@ -0,0 +1,41 @@
package api
/*
*
message LoginRequest {
string username = 1;
bytes password = 2;
string my_id = 4;
string my_name = 5;
OptionMessage option = 6;
oneof union {
FileTransfer file_transfer = 7;
PortForward port_forward = 8;
}
bool video_ack_required = 9;
uint64 session_id = 10;
string version = 11;
OSLogin os_login = 12;
string my_platform = 13;
bytes hwid = 14;
}
*/
type LoginForm struct {
Username string `json:"username" validate:"required,gte=4,lte=10" label:"用户名"`
Password string `json:"password,omitempty" validate:"gte=4,lte=20" label:"密码"`
}
type UserListQuery struct {
Page uint `json:"page" form:"page" validate:"required" label:"页码"`
PageSize uint `json:"page_size" form:"page_size" validate:"required" label:"每页数量"`
Status int `json:"status" form:"status" label:"状态"`
Accessible string `json:"accessible" form:"accessible"`
}
type PeerListQuery struct {
Page uint `json:"page" form:"page" validate:"required" label:"页码"`
PageSize uint `json:"page_size" form:"page_size" validate:"required" label:"每页数量"`
Status int `json:"status" form:"status" label:"状态"`
Accessible string `json:"accessible" form:"accessible"`
}

View File

@@ -0,0 +1,13 @@
package admin
type LoginPayload struct {
Username string `json:"username"`
Token string `json:"token"`
RouteNames []string `json:"route_names"`
Nickname string `json:"nickname"`
}
var UserRouteNames = []string{
"MyTagList", "MyAddressBookList",
}
var AdminRouteNames = []string{"*"}

9
http/response/api/ab.go Normal file
View File

@@ -0,0 +1,9 @@
package api
import "Gwen/model"
type AbList struct {
Peers []*model.AddressBook `json:"peers,omitempty"`
Tags []string `json:"tags,omitempty"`
TagColors string `json:"tag_colors,omitempty"`
}

74
http/response/api/peer.go Normal file
View File

@@ -0,0 +1,74 @@
package api
import "Gwen/model"
/*
GroupPeerPayload
https://github.com/rustdesk/rustdesk/blob/master/flutter/lib/common/hbbs/hbbs.dart#L64
String id = '';
Map<String, dynamic> info = {};
int? status;
String user = '';
String user_name = '';
String note = '';
PeerPayload.fromJson(Map<String, dynamic> json)
: id = json['id'] ?? '',
info = (json['info'] is Map<String, dynamic>) ? json['info'] : {},
status = json['status'],
user = json['user'] ?? '',
user_name = json['user_name'] ?? '',
note = json['note'] ?? '';
static Peer toPeer(GroupPeerPayload p) {
return Peer.fromJson({
"id": p.id,
'loginName': p.user_name,
"username": p.info['username'] ?? '',
"platform": _platform(p.info['os']),
"hostname": p.info['device_name'],
});
}
*/
type GroupPeerPayload struct {
Id string `json:"id"`
Info *PeerPayloadInfo `json:"info"`
Status int `json:"status"`
User string `json:"user"`
UserName string `json:"user_name"`
Note string `json:"note"`
}
type PeerPayloadInfo struct {
DeviceName string `json:"device_name"`
Os string `json:"os"`
Username string `json:"username"`
}
func (gpp *GroupPeerPayload) FromAddressBook(a *model.AddressBook, username string) {
gpp.Id = a.Id
os := a.Platform
if a.Platform == "Mac OS" {
os = "MacOS"
}
gpp.Info = &PeerPayloadInfo{
DeviceName: a.Hostname,
Os: os,
Username: a.Username,
}
gpp.UserName = username
}
//func (gpp *GroupPeerPayload) FromPeer(p *model.Peer) {
// gpp.Id = p.Id
// gpp.Info = &PeerPayloadInfo{
// DeviceName: p.Hostname,
// Os: p.Os,
// Username: p.Username,
// }
// gpp.Note = ""
// if p.User.Id != 0 {
// //gpp.User = p.User.Username
// gpp.UserName = p.User.Username
// }
//}

55
http/response/api/user.go Normal file
View File

@@ -0,0 +1,55 @@
package api
import "Gwen/model"
/*
pub enum UserStatus {
Disabled = 0,
Normal = 1,
Unverified = -1,
}
*/
/*
UserPayload
String name = ”;
String email = ”;
String note = ”;
UserStatus status;
bool isAdmin = false;
*/
type UserPayload struct {
Name string `json:"name"`
Email string `json:"email"`
Note string `json:"note"`
IsAdmin *bool `json:"is_admin"`
Status int `json:"status"`
}
func (up *UserPayload) FromUser(user *model.User) *UserPayload {
up.Name = user.Username
up.IsAdmin = user.IsAdmin
up.Status = int(user.Status)
return up
}
/*
class HttpType {
static const kAuthReqTypeAccount = "account";
static const kAuthReqTypeMobile = "mobile";
static const kAuthReqTypeSMSCode = "sms_code";
static const kAuthReqTypeEmailCode = "email_code";
static const kAuthReqTypeTfaCode = "tfa_code";
static const kAuthResTypeToken = "access_token";
static const kAuthResTypeEmailCheck = "email_check";
static const kAuthResTypeTfaCheck = "tfa_check";
}
*/
type LoginRes struct {
Type string `json:"type"`
AccessToken string `json:"access_token"`
User UserPayload `json:"user"`
Secret string `json:"secret"`
TfaType string `json:"tfa_type"`
}

View File

@@ -0,0 +1,55 @@
package api
import (
"Gwen/model"
"time"
)
// type T struct {
// Field1 struct {
// ViewStyle string `json:"view-style"`
// Tm int64 `json:"tm"`
// Info struct {
// Username string `json:"username"`
// Hostname string `json:"hostname"`
// Platform string `json:"platform"`
// Displays []struct {
// X int `json:"x"`
// Y int `json:"y"`
// Width int `json:"width"`
// Height int `json:"height"`
// Name string `json:"name"`
// Online bool `json:"online"`
// } `json:"displays"`
// CurrentDisplay int `json:"current_display"`
// SasEnabled bool `json:"sas_enabled"`
// Version string `json:"version"`
// ConnId int `json:"conn_id"`
// Features struct {
// PrivacyMode bool `json:"privacy_mode"`
// } `json:"features"`
// } `json:"info"`
// } `json:"1799928825"`
// }
type WebClientPeerPayload struct {
ViewStyle string `json:"view-style"`
Tm int64 `json:"tm"`
Info WebClientPeerInfoPayload `json:"info"`
}
type WebClientPeerInfoPayload struct {
Username string `json:"username"`
Hostname string `json:"hostname"`
Platform string `json:"platform"`
}
func (wcpp *WebClientPeerPayload) FromAddressBook(a *model.AddressBook) {
wcpp.ViewStyle = "shrink"
wcpp.Tm = time.Now().UnixNano()
wcpp.Info = WebClientPeerInfoPayload{
Username: a.Username,
Hostname: a.Hostname,
Platform: a.Platform,
}
}

53
http/response/response.go Normal file
View File

@@ -0,0 +1,53 @@
package response
import (
"github.com/gin-gonic/gin"
"net/http"
)
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
type PageData struct {
Page int `json:"page"`
Total int `json:"total"`
List interface{} `json:"list"`
}
type DataResponse struct {
Total uint `json:"total"`
Data interface{} `json:"data"`
}
type ErrorResponse struct {
Error string `json:"error"`
}
func SendResponse(c *gin.Context, code int, message string, data interface{}) {
c.JSON(http.StatusOK, Response{
code, message, data,
})
}
func Success(c *gin.Context, data interface{}) {
SendResponse(c, 0, "success", data)
}
func Fail(c *gin.Context, code int, message string) {
SendResponse(c, code, message, nil)
}
func Error(c *gin.Context, message string) {
c.JSON(http.StatusBadRequest, ErrorResponse{
Error: message,
})
}
type ServerConfigResponse struct {
IdServer string `json:"id_server"`
Key string `json:"key"`
RelayServer string `json:"relay_server"`
ApiServer string `json:"api_server"`
}

118
http/router/admin.go Normal file
View File

@@ -0,0 +1,118 @@
package router
import (
_ "Gwen/docs/admin"
"Gwen/http/controller/admin"
"Gwen/http/middleware"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
func Init(g *gin.Engine) {
//swagger
//g.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
g.GET("/admin/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, ginSwagger.InstanceName("admin")))
adg := g.Group("/api/admin")
LoginBind(adg)
adg.Use(middleware.AdminAuth())
//FileBind(adg)
UserBind(adg)
GroupBind(adg)
TagBind(adg)
AddressBookBind(adg)
PeerBind(adg)
rs := &admin.Rustdesk{}
adg.GET("/server-config", rs.ServerConfig)
//访问静态文件
//g.StaticFS("/upload", http.Dir(global.Config.Gin.ResourcesPath+"/upload"))
}
func LoginBind(rg *gin.RouterGroup) {
cont := &admin.Login{}
rg.POST("/login", cont.Login)
rg.POST("/logout", cont.Logout)
}
func UserBind(rg *gin.RouterGroup) {
aR := rg.Group("/user")
{
cont := &admin.User{}
aR.GET("/current", cont.Current)
aR.POST("/changeCurPwd", cont.ChangeCurPwd)
}
aRP := rg.Group("/user").Use(middleware.AdminPrivilege())
{
cont := &admin.User{}
aRP.GET("/list", cont.List)
aRP.GET("/detail/:id", cont.Detail)
aRP.POST("/create", cont.Create)
aRP.POST("/update", cont.Update)
aRP.POST("/delete", cont.Delete)
aRP.POST("/changePwd", cont.UpdatePassword)
}
}
func GroupBind(rg *gin.RouterGroup) {
aR := rg.Group("/group").Use(middleware.AdminPrivilege())
{
cont := &admin.Group{}
aR.GET("/list", cont.List)
aR.GET("/detail/:id", cont.Detail)
aR.POST("/create", cont.Create)
aR.POST("/update", cont.Update)
aR.POST("/delete", cont.Delete)
}
}
func TagBind(rg *gin.RouterGroup) {
aR := rg.Group("/tag")
{
cont := &admin.Tag{}
aR.GET("/list", cont.List)
aR.GET("/detail/:id", cont.Detail)
aR.POST("/create", cont.Create)
aR.POST("/update", cont.Update)
aR.POST("/delete", cont.Delete)
}
}
func AddressBookBind(rg *gin.RouterGroup) {
aR := rg.Group("/address_book")
{
cont := &admin.AddressBook{}
aR.GET("/list", cont.List)
aR.GET("/detail/:id", cont.Detail)
aR.POST("/create", cont.Create)
aR.POST("/update", cont.Update)
aR.POST("/delete", cont.Delete)
}
}
func PeerBind(rg *gin.RouterGroup) {
aR := rg.Group("/peer")
{
cont := &admin.Peer{}
aR.GET("/list", cont.List)
aR.GET("/detail/:id", cont.Detail)
aR.POST("/create", cont.Create)
aR.POST("/update", cont.Update)
aR.POST("/delete", cont.Delete)
}
}
/*
func FileBind(rg *gin.RouterGroup) {
aR := rg.Group("/file")
{
cont := &admin.File{}
aR.POST("/notify", cont.Notify)
aR.OPTIONS("/oss_token", nil)
aR.OPTIONS("/upload", nil)
aR.GET("/oss_token", cont.OssToken)
aR.POST("/upload", cont.Upload)
}
}*/

73
http/router/api.go Normal file
View File

@@ -0,0 +1,73 @@
package router
import (
_ "Gwen/docs/api"
"Gwen/global"
"Gwen/http/controller/api"
"Gwen/http/middleware"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"net/http"
)
func ApiInit(g *gin.Engine) {
//g.Use(middleware.Cors())
//swagger
g.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, ginSwagger.InstanceName("api")))
frg := g.Group("/api")
frg.Use(middleware.Cors())
frg.OPTIONS("/*any", nil)
i := &api.Index{}
frg.GET("/", i.Index)
frg.POST("/heartbeat", i.Heartbeat)
{
l := &api.Login{}
// 如果返回oidc则可以通过oidc登录
frg.GET("/login-options", l.LoginOptions)
frg.POST("/login", l.Login)
}
{
pe := &api.Peer{}
//提交系统信息
frg.POST("/sysinfo", pe.SysInfo)
}
frg.Use(middleware.RustAuth())
{
w := &api.WebClient{}
frg.POST("/server-config", w.ServerConfig)
}
{
u := &api.User{}
frg.GET("/user/info", u.Info)
frg.POST("/currentUser", u.Info)
}
{
l := &api.Login{}
frg.POST("/logout", l.Logout)
}
{
gr := &api.Group{}
frg.GET("/users", gr.Users)
frg.GET("/peers", gr.Peers)
}
{
ab := &api.Ab{}
//获取地址
frg.GET("/ab", ab.Ab)
//更新地址
frg.POST("/ab", ab.UpAb)
}
//访问静态文件
g.StaticFS("/upload", http.Dir(global.Config.Gin.ResourcesPath+"/public/upload"))
}

15
http/router/router.go Normal file
View File

@@ -0,0 +1,15 @@
package router
import (
"Gwen/global"
"Gwen/http/controller/web"
"github.com/gin-gonic/gin"
"net/http"
)
func WebInit(g *gin.Engine) {
i := &web.Index{}
g.GET("/webclient-config/index.js", i.ConfigJs)
g.StaticFS("/webclient", http.Dir(global.Config.Gin.ResourcesPath+"/web"))
g.StaticFS("/_admin", http.Dir(global.Config.Gin.ResourcesPath+"/admin"))
}

12
http/run.go Normal file
View File

@@ -0,0 +1,12 @@
//go:build !windows
package http
import (
"github.com/fvbock/endless"
"github.com/gin-gonic/gin"
)
func Run(g *gin.Engine, addr string) {
endless.ListenAndServe(addr, g)
}

11
http/run_win.go Normal file
View File

@@ -0,0 +1,11 @@
//go:build windows
package http
import (
"github.com/gin-gonic/gin"
)
func Run(g *gin.Engine, addr string) {
g.Run(addr)
}

71
lib/cache/cache.go vendored Normal file
View File

@@ -0,0 +1,71 @@
package cache
import (
"encoding/json"
)
type Handler interface {
Get(key string, value interface{}) error
Set(key string, value interface{}, exp int) error
Gc() error
}
// MaxTimeOut 最大超时时间
const (
TypeMem = "memory"
TypeRedis = "redis"
TypeFile = "file"
MaxTimeOut = 365 * 24 * 3600
)
func New(typ string) Handler {
var cache Handler
switch typ {
case TypeFile:
cache = NewFileCache()
case TypeRedis:
cache = new(RedisCache)
case TypeMem: // memory
cache = NewMemoryCache(0)
default:
cache = NewMemoryCache(0)
}
return cache
}
func EncodeValue(value interface{}) (string, error) {
/*if v, ok := value.(string); ok {
return v, nil
}
if v, ok := value.([]byte); ok {
return string(v), nil
}*/
b, err := json.Marshal(value)
if err != nil {
return "", err
}
return string(b), nil
}
func DecodeValue(value string, rtv interface{}) error {
//判断rtv的类型是否是string如果是string直接赋值并返回
/*switch rtv.(type) {
case *string:
*(rtv.(*string)) = value
return nil
case *[]byte:
*(rtv.(*[]byte)) = []byte(value)
return nil
//struct
case *interface{}:
err := json.Unmarshal(([]byte)(value), rtv)
return err
default:
err := json.Unmarshal(([]byte)(value), rtv)
return err
}
*/
err := json.Unmarshal(([]byte)(value), rtv)
return err
}

92
lib/cache/cache_test.go vendored Normal file
View File

@@ -0,0 +1,92 @@
package cache
import (
"fmt"
"github.com/go-redis/redis/v8"
"reflect"
"testing"
)
func TestSimpleCache(t *testing.T) {
type st struct {
A string
B string
}
items := map[string]interface{}{}
items["a"] = "b"
items["b"] = "c"
ab := &st{
A: "a",
B: "b",
}
items["ab"] = *ab
a := items["a"]
fmt.Println(a)
b := items["b"]
fmt.Println(b)
ab.A = "aa"
ab2 := st{}
ab2 = (items["ab"]).(st)
fmt.Println(ab2, reflect.TypeOf(ab2))
}
func TestFileCacheSet(t *testing.T) {
fc := New("file")
err := fc.Set("123", "ddd", 0)
if err != nil {
fmt.Println(err.Error())
t.Fatalf("写入失败")
}
}
func TestFileCacheGet(t *testing.T) {
fc := New("file")
err := fc.Set("123", "45156", 300)
if err != nil {
t.Fatalf("写入失败")
}
res := ""
err = fc.Get("123", &res)
if err != nil {
t.Fatalf("读取失败")
}
fmt.Println("res", res)
}
func TestRedisCacheSet(t *testing.T) {
rc := NewRedis(&redis.Options{
Addr: "192.168.1.168:6379",
Password: "", // no password set
DB: 0, // use default DB
})
err := rc.Set("123", "ddd", 0)
if err != nil {
fmt.Println(err.Error())
t.Fatalf("写入失败")
}
}
func TestRedisCacheGet(t *testing.T) {
rc := NewRedis(&redis.Options{
Addr: "192.168.1.168:6379",
Password: "", // no password set
DB: 0, // use default DB
})
err := rc.Set("123", "451156", 300)
if err != nil {
t.Fatalf("写入失败")
}
res := ""
err = rc.Get("123", &res)
if err != nil {
t.Fatalf("读取失败")
}
fmt.Println("res", res)
}

103
lib/cache/file.go vendored Normal file
View File

@@ -0,0 +1,103 @@
package cache
import (
"crypto/md5"
"fmt"
"os"
"sync"
"time"
)
type FileCache struct {
mu sync.Mutex
locks map[string]*sync.Mutex
Dir string
}
func (fc *FileCache) getLock(key string) *sync.Mutex {
fc.mu.Lock()
defer fc.mu.Unlock()
if fc.locks == nil {
fc.locks = make(map[string]*sync.Mutex)
}
if _, ok := fc.locks[key]; !ok {
fc.locks[key] = new(sync.Mutex)
}
return fc.locks[key]
}
func (c *FileCache) Get(key string, value interface{}) error {
data, _ := c.getValue(key)
err := DecodeValue(data, value)
return err
}
// 获取值,如果文件不存在或者过期,返回空,过滤掉错误
func (c *FileCache) getValue(key string) (string, error) {
f := c.fileName(key)
fileInfo, err := os.Stat(f)
if err != nil {
//文件不存在
return "", nil
}
difT := time.Now().Sub(fileInfo.ModTime())
if difT >= 0 {
os.Remove(f)
return "", nil
}
data, err := os.ReadFile(f)
if err != nil {
return "", nil
}
return string(data), nil
}
// 保存值
func (c *FileCache) saveValue(key string, value string, exp int) error {
f := c.fileName(key)
lock := c.getLock(f)
lock.Lock()
defer lock.Unlock()
err := os.WriteFile(f, ([]byte)(value), 0644)
if err != nil {
return err
}
if exp <= 0 {
exp = MaxTimeOut
}
expFromNow := time.Now().Add(time.Duration(exp) * time.Second)
err = os.Chtimes(f, expFromNow, expFromNow)
return err
}
func (c *FileCache) Set(key string, value interface{}, exp int) error {
str, err := EncodeValue(value)
if err != nil {
return err
}
err = c.saveValue(key, str, exp)
return err
}
func (c *FileCache) SetDir(path string) {
c.Dir = path
}
func (c *FileCache) fileName(key string) string {
f := c.Dir + string(os.PathSeparator) + fmt.Sprintf("%x", md5.Sum([]byte(key)))
return f
}
func (c *FileCache) Gc() error {
//检查文件过期时间,并删除
return nil
}
func NewFileCache() *FileCache {
return &FileCache{
locks: make(map[string]*sync.Mutex),
Dir: os.TempDir(),
}
}

94
lib/cache/file_test.go vendored Normal file
View File

@@ -0,0 +1,94 @@
package cache
import (
"fmt"
"reflect"
"testing"
)
func TestFileSet(t *testing.T) {
fc := NewFileCache()
err := fc.Set("123", "ddd", 0)
if err != nil {
fmt.Println(err.Error())
t.Fatalf("写入失败")
}
}
func TestFileGet(t *testing.T) {
fc := NewFileCache()
res := ""
err := fc.Get("123", &res)
if err != nil {
fmt.Println(err.Error())
t.Fatalf("读取失败")
}
fmt.Println("res", res)
}
func TestFileSetGet(t *testing.T) {
fc := NewFileCache()
err := fc.Set("key1", "ddd", 0)
res := ""
err = fc.Get("key1", &res)
if err != nil {
fmt.Println(err.Error())
t.Fatalf("读取失败")
}
fmt.Println("res", res)
}
func TestFileGetJson(t *testing.T) {
fc := NewFileCache()
old := &r{
A: "a", B: "b",
}
fc.Set("123", old, 0)
res := &r{}
err2 := fc.Get("123", res)
fmt.Println("res", res)
if err2 != nil {
t.Fatalf("读取失败" + err2.Error())
}
}
func TestFileSetGetJson(t *testing.T) {
fc := NewFileCache()
old_rr := &rr{AA: "aa", BB: "bb"}
old := &r{
A: "a", B: "b",
R: old_rr,
}
err := fc.Set("123", old, 300)
if err != nil {
t.Fatalf("写入失败")
}
//old_rr.AA = "aaa"
fmt.Println("old_rr", old)
res := &r{}
err2 := fc.Get("123", res)
fmt.Println("res", res)
if err2 != nil {
t.Fatalf("读取失败" + err2.Error())
}
if !reflect.DeepEqual(res, old) {
t.Fatalf("读取错误")
}
}
func BenchmarkSet(b *testing.B) {
fc := NewFileCache()
b.ResetTimer()
for i := 0; i < b.N; i++ {
fc.Set("123", "{dsv}", 1000)
}
}
func BenchmarkGet(b *testing.B) {
fc := NewFileCache()
b.ResetTimer()
v := ""
for i := 0; i < b.N; i++ {
fc.Get("123", &v)
}
}

215
lib/cache/memory.go vendored Normal file
View File

@@ -0,0 +1,215 @@
package cache
import (
"container/heap"
"container/list"
"errors"
"reflect"
"sync"
"time"
)
type MemoryCache struct {
data map[string]*CacheItem
ll *list.List // 用于实现LRU
pq PriorityQueue // 用于实现TTL
quit chan struct{}
mu sync.Mutex
maxBytes int64
usedBytes int64
}
type CacheItem struct {
Key string
Value string
Expiration int64
Index int
ListEle *list.Element
}
type PriorityQueue []*CacheItem
func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool {
return pq[i].Expiration < pq[j].Expiration
}
func (pq PriorityQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
pq[i].Index = i
pq[j].Index = j
}
func (pq *PriorityQueue) Push(x interface{}) {
item := x.(*CacheItem)
item.Index = len(*pq)
*pq = append(*pq, item)
}
func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
old[n-1] = nil // avoid memory leak
item.Index = -1 // for safety
*pq = old[0 : n-1]
return item
}
func (m *MemoryCache) Get(key string, value interface{}) error {
// 使用反射将存储的值设置到传入的指针变量中
val := reflect.ValueOf(value)
if val.Kind() != reflect.Ptr {
return errors.New("value must be a pointer")
}
//设为空值
val.Elem().Set(reflect.Zero(val.Elem().Type()))
m.mu.Lock()
defer m.mu.Unlock()
if m.data == nil {
return nil
}
if item, ok := m.data[key]; ok {
if item.Expiration < time.Now().UnixNano() {
m.deleteItem(item)
return nil
}
//移动到队列尾部
m.ll.MoveToBack(item.ListEle)
err := DecodeValue(item.Value, value)
if err != nil {
return err
}
}
return nil
}
func (m *MemoryCache) Set(key string, value interface{}, exp int) error {
m.mu.Lock()
defer m.mu.Unlock()
v, err := EncodeValue(value)
if err != nil {
return err
}
//key 所占用的内存
keyBytes := int64(len(key))
//value所占用的内存空间大小
valueBytes := int64(len(v))
//判断是否超过最大内存限制
if m.maxBytes != 0 && m.maxBytes < keyBytes+valueBytes {
return errors.New("exceed maxBytes")
}
m.usedBytes += keyBytes + valueBytes
if m.maxBytes != 0 && m.usedBytes > m.maxBytes {
m.RemoveOldest()
}
if exp <= 0 {
exp = MaxTimeOut
}
expiration := time.Now().Add(time.Duration(exp) * time.Second).UnixNano()
item, exists := m.data[key]
if exists {
item.Value = v
item.Expiration = expiration
heap.Fix(&m.pq, item.Index)
m.ll.MoveToBack(item.ListEle)
} else {
ele := m.ll.PushBack(key)
item = &CacheItem{
Key: key,
Value: v,
Expiration: expiration,
ListEle: ele,
}
m.data[key] = item
heap.Push(&m.pq, item)
}
return nil
}
func (m *MemoryCache) RemoveOldest() {
for m.maxBytes != 0 && m.usedBytes > m.maxBytes {
elem := m.ll.Front()
if elem != nil {
key := elem.Value.(string)
item := m.data[key]
m.deleteItem(item)
}
}
}
// evictExpiredItems removes all expired items from the cache.
func (m *MemoryCache) evictExpiredItems() {
m.mu.Lock()
defer m.mu.Unlock()
now := time.Now().UnixNano()
for m.pq.Len() > 0 {
item := m.pq[0]
if item.Expiration > now {
break
}
m.deleteItem(item)
}
}
// startEviction starts a goroutine that evicts expired items from the cache.
func (m *MemoryCache) startEviction() {
ticker := time.NewTicker(1 * time.Second)
go func() {
for {
select {
case <-ticker.C:
m.evictExpiredItems()
case <-m.quit:
ticker.Stop()
return
}
}
}()
}
// stopEviction 停止定时清理
func (m *MemoryCache) stopEviction() {
close(m.quit)
}
// deleteItem removes a key from the cache.
func (m *MemoryCache) deleteItem(item *CacheItem) {
m.ll.Remove(item.ListEle)
m.usedBytes -= int64(len(item.Key)) + int64(len(item.Value))
heap.Remove(&m.pq, item.Index)
delete(m.data, item.Key)
}
func (m *MemoryCache) Gc() error {
m.mu.Lock()
defer m.mu.Unlock()
m.data = make(map[string]*CacheItem)
m.ll = list.New()
m.pq = make(PriorityQueue, 0)
heap.Init(&m.pq)
m.usedBytes = 0
return nil
}
// NewMemoryCache creates a new MemoryCache.default maxBytes is 0, means no limit.
func NewMemoryCache(maxBytes int64) *MemoryCache {
cache := &MemoryCache{
data: make(map[string]*CacheItem),
pq: make(PriorityQueue, 0),
quit: make(chan struct{}),
ll: list.New(),
maxBytes: maxBytes,
}
heap.Init(&cache.pq)
cache.startEviction()
return cache
}

107
lib/cache/memory_test.go vendored Normal file
View File

@@ -0,0 +1,107 @@
package cache
import (
"fmt"
"testing"
"time"
)
func TestMemorySet(t *testing.T) {
mc := NewMemoryCache(0)
err := mc.Set("123", "44567", 0)
if err != nil {
fmt.Println(err.Error())
t.Fatalf("写入失败")
}
}
func TestMemoryGet(t *testing.T) {
mc := NewMemoryCache(0)
mc.Set("123", "44567", 0)
res := ""
err := mc.Get("123", &res)
fmt.Println("res", res)
if err != nil {
t.Fatalf("读取失败 " + err.Error())
}
if res != "44567" {
t.Fatalf("读取错误")
}
}
func TestMemorySetExpGet(t *testing.T) {
mc := NewMemoryCache(0)
//mc.stopEviction()
mc.Set("1", "10", 10)
mc.Set("2", "5", 5)
err := mc.Set("3", "3", 3)
if err != nil {
t.Fatalf("写入失败")
}
res := ""
err = mc.Get("3", &res)
if err != nil {
t.Fatalf("读取失败" + err.Error())
}
fmt.Println("res 3", res)
time.Sleep(4 * time.Second)
//res = ""
err = mc.Get("3", &res)
if err != nil {
t.Fatalf("读取失败" + err.Error())
}
fmt.Println("res 3", res)
err = mc.Get("2", &res)
if err != nil {
t.Fatalf("读取失败" + err.Error())
}
fmt.Println("res 2", res)
err = mc.Get("1", &res)
if err != nil {
t.Fatalf("读取失败" + err.Error())
}
fmt.Println("res 1", res)
}
func TestMemoryLru(t *testing.T) {
mc := NewMemoryCache(18)
mc.Set("1", "1111", 10)
mc.Set("2", "2222", 5)
//读取一次2就会被放到最后
mc.Get("1", nil)
err := mc.Set("3", "三", 3)
if err != nil {
//t.Fatalf("写入失败")
}
res := ""
err = mc.Get("3", &res)
if err != nil {
t.Fatalf("读取失败" + err.Error())
}
fmt.Println("res3", res)
res = ""
err = mc.Get("2", &res)
if err != nil {
t.Fatalf("读取失败" + err.Error())
}
fmt.Println("res2", res)
res = ""
err = mc.Get("1", &res)
if err != nil {
t.Fatalf("读取失败" + err.Error())
}
fmt.Println("res1", res)
}
func BenchmarkMemorySet(b *testing.B) {
mc := NewMemoryCache(0)
b.ResetTimer()
for i := 0; i < b.N; i++ {
key := fmt.Sprintf("key%d", i)
value := fmt.Sprintf("value%d", i)
mc.Set(key, value, 1000)
}
}

49
lib/cache/redis.go vendored Normal file
View File

@@ -0,0 +1,49 @@
package cache
import (
"context"
"github.com/go-redis/redis/v8"
"time"
)
var ctx = context.Background()
type RedisCache struct {
rdb *redis.Client
}
func RedisCacheInit(conf *redis.Options) *RedisCache {
c := &RedisCache{}
c.rdb = redis.NewClient(conf)
return c
}
func (c *RedisCache) Get(key string, value interface{}) error {
data, err := c.rdb.Get(ctx, key).Result()
if err != nil {
return err
}
err1 := DecodeValue(data, value)
return err1
}
func (c *RedisCache) Set(key string, value interface{}, exp int) error {
str, err := EncodeValue(value)
if err != nil {
return err
}
if exp <= 0 {
exp = MaxTimeOut
}
_, err1 := c.rdb.Set(ctx, key, str, time.Duration(exp)*time.Second).Result()
return err1
}
func (c *RedisCache) Gc() error {
return nil
}
func NewRedis(conf *redis.Options) *RedisCache {
cache := RedisCacheInit(conf)
return cache
}

94
lib/cache/redis_test.go vendored Normal file
View File

@@ -0,0 +1,94 @@
package cache
import (
"fmt"
"github.com/go-redis/redis/v8"
"reflect"
"testing"
)
func TestRedisSet(t *testing.T) {
//rc := New("redis")
rc := RedisCacheInit(&redis.Options{
Addr: "192.168.1.168:6379",
Password: "", // no password set
DB: 0, // use default DB
})
err := rc.Set("123", "ddd", 0)
if err != nil {
fmt.Println(err.Error())
t.Fatalf("写入失败")
}
}
func TestRedisGet(t *testing.T) {
rc := RedisCacheInit(&redis.Options{
Addr: "192.168.1.168:6379",
Password: "", // no password set
DB: 0, // use default DB
})
err := rc.Set("123", "451156", 300)
if err != nil {
t.Fatalf("写入失败")
}
res := ""
err = rc.Get("123", &res)
if err != nil {
t.Fatalf("读取失败")
}
fmt.Println("res", res)
}
func TestRedisGetJson(t *testing.T) {
rc := RedisCacheInit(&redis.Options{
Addr: "192.168.1.168:6379",
Password: "", // no password set
DB: 0, // use default DB
})
type r struct {
Aa string `json:"a"`
B string `json:"c"`
}
old := &r{
Aa: "ab", B: "cdc",
}
err := rc.Set("1233", old, 300)
if err != nil {
t.Fatalf("写入失败")
}
res := &r{}
err2 := rc.Get("1233", res)
if err2 != nil {
t.Fatalf("读取失败")
}
if !reflect.DeepEqual(res, old) {
t.Fatalf("读取错误")
}
fmt.Println(res, res.Aa)
}
func BenchmarkRSet(b *testing.B) {
rc := RedisCacheInit(&redis.Options{
Addr: "192.168.1.168:6379",
Password: "", // no password set
DB: 0, // use default DB
})
b.ResetTimer()
for i := 0; i < b.N; i++ {
rc.Set("123", "{dsv}", 1000)
}
}
func BenchmarkRGet(b *testing.B) {
rc := RedisCacheInit(&redis.Options{
Addr: "192.168.1.168:6379",
Password: "", // no password set
DB: 0, // use default DB
})
b.ResetTimer()
v := ""
for i := 0; i < b.N; i++ {
rc.Get("123", &v)
}
}

65
lib/cache/simple_cache.go vendored Normal file
View File

@@ -0,0 +1,65 @@
package cache
import (
"errors"
"reflect"
"sync"
)
// 此处实现了一个简单的缓存,用于测试
// SimpleCache is a simple cache implementation
type SimpleCache struct {
data map[string]interface{}
mu sync.Mutex
maxBytes int64
usedBytes int64
}
func (s *SimpleCache) Get(key string, value interface{}) error {
s.mu.Lock()
defer s.mu.Unlock()
// 使用反射将存储的值设置到传入的指针变量中
val := reflect.ValueOf(value)
if val.Kind() != reflect.Ptr {
return errors.New("value must be a pointer")
}
v, ok := s.data[key]
if !ok {
//设为空值
val.Elem().Set(reflect.Zero(val.Elem().Type()))
return nil
}
vval := reflect.ValueOf(v)
if val.Elem().Type() != vval.Type() {
//设为空值
val.Elem().Set(reflect.Zero(val.Elem().Type()))
return nil
}
val.Elem().Set(reflect.ValueOf(v))
return nil
}
func (s *SimpleCache) Set(key string, value interface{}, exp int) error {
s.mu.Lock()
defer s.mu.Unlock()
// 检查传入的值是否是指针,如果是则取其值
val := reflect.ValueOf(value)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
s.data[key] = val.Interface()
return nil
}
func (s *SimpleCache) Gc() error {
return nil
}
func NewSimpleCache() *SimpleCache {
return &SimpleCache{
data: make(map[string]interface{}),
}
}

108
lib/cache/simple_cache_test.go vendored Normal file
View File

@@ -0,0 +1,108 @@
package cache
import (
"fmt"
"testing"
)
func TestSimpleCache_Set(t *testing.T) {
s := NewSimpleCache()
err := s.Set("key", "value", 0)
if err != nil {
t.Fatalf("写入失败")
}
err = s.Set("key", 111, 0)
if err != nil {
t.Fatalf("写入失败")
}
}
func TestSimpleCache_Get(t *testing.T) {
s := NewSimpleCache()
err := s.Set("key", "value", 0)
value := ""
err = s.Get("key", &value)
fmt.Println("value", value)
if err != nil {
t.Fatalf("读取失败")
}
err = s.Set("key1", 11, 0)
value1 := 0
err = s.Get("key1", &value1)
fmt.Println("value1", value1)
if err != nil {
t.Fatalf("读取失败")
}
err = s.Set("key2", []byte{'a', 'b'}, 0)
value2 := []byte{}
err = s.Get("key2", &value2)
fmt.Println("value2", string(value2))
if err != nil {
t.Fatalf("读取失败")
}
err = s.Set("key3", 33.33, 0)
var value3 int
err = s.Get("key3", &value3)
fmt.Println("value3", value3)
if err != nil {
t.Fatalf("读取失败")
}
}
type r struct {
A string `json:"a"`
B string `json:"b"`
R *rr `json:"r"`
}
type r2 struct {
A string `json:"a"`
B string `json:"b"`
}
type rr struct {
AA string `json:"aa"`
BB string `json:"bb"`
}
func TestSimpleCache_GetStruct(t *testing.T) {
s := NewSimpleCache()
old_rr := &rr{
AA: "aa", BB: "bb",
}
old := &r{
A: "ab", B: "cdc",
R: old_rr,
}
err := s.Set("key", old, 300)
if err != nil {
t.Fatalf("写入失败")
}
res := &r{}
err2 := s.Get("key", res)
fmt.Println("res", res)
if err2 != nil {
t.Fatalf("读取失败" + err2.Error())
}
//修改原始值,看后面是否会变化
old.A = "aa"
old_rr.AA = "aaa"
fmt.Println("old", old)
res2 := &r{}
err3 := s.Get("key", res2)
fmt.Println("res2", res2, res2.R.AA, res2.R.BB)
if err3 != nil {
t.Fatalf("读取失败" + err3.Error())
}
//if reflect.DeepEqual(res, old) {
// t.Fatalf("读取错误")
//}
}

61
lib/jwt/jwt.go Normal file
View File

@@ -0,0 +1,61 @@
package jwt
import (
"crypto/rsa"
"github.com/golang-jwt/jwt/v5"
"os"
"time"
)
type Jwt struct {
privateKey *rsa.PrivateKey
TokenExpireDuration time.Duration
}
type UserClaims struct {
UserId uint `json:"user_id"`
jwt.RegisteredClaims
}
func NewJwt(privateKeyFile 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{
privateKey: privateKey,
TokenExpireDuration: tokenExpireDuration,
}
}
func (s *Jwt) GenerateToken(userId uint) string {
t := jwt.NewWithClaims(jwt.SigningMethodRS256,
UserClaims{
UserId: userId,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.TokenExpireDuration)),
},
})
token, err := t.SignedString(s.privateKey)
if err != nil {
return ""
}
return token
}
func (s *Jwt) ParseToken(tokenString string) (uint, error) {
token, err := jwt.ParseWithClaims(tokenString, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
return s.privateKey.Public(), nil
})
if err != nil {
return 0, err
}
if claims, ok := token.Claims.(*UserClaims); ok && token.Valid {
return claims.UserId, nil
}
return 0, err
}

80
lib/jwt/jwt_test.go Normal file
View File

@@ -0,0 +1,80 @@
package jwt
import (
"fmt"
"testing"
"time"
)
var pk = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAnJpq2Sy91iGW3+EuG4V2ke59tITpGINzht0rO8WiRwu11W4p
wakS4K4BbjvmC8YjaxXhKE5LHDw0IXvTdIDN7Fuu4qs9xWXIoK+nC3qWrVBtj/1o
RJrYme1NenTXEgPlN1FOU6/9XQGgvb+1MSNqxknYo7183mHACvsIIuSTMEFhUbUw
XYVQrCtACUILZ9wIDOEzclIY2ZPMTnL1vkvfj629KwGtAvpEyc96Y/HMSH5/VkiG
p6L+k+NSjco9HntAGYTiQkfranvdqxRDUsKS53SbV3QSz1zc0l5OEyZDuxFTL7UC
7v0G/HVqz6mLpMje756PG/WEpwa/lADc/8FJ5QIDAQABAoIBAEsqUt6qevOsa55J
lrfe92pT7kIXCUqazXiN75Jg6eLv2/b1SVWKsWTmIAmo9mHwWE+t0MRnz+VdgCgS
JwxkRnKMDwT87Eky8Xku1h7MWEYXtH7IQqOrLwuyut1r907OT9adT9sbPaDGh0CM
I4vSVA2YpELzUFvszyB2HRGiZINkHfdLsNxUKsHJOdXbv82RItwzmCYcZismnR3J
P8THn06eoBNtlqwdFziuREOzjNnj6J/3glhR5mu4c4+AJoj0hmVaBDfac3GsQsbP
x79QQPrUqH9UZ4szubYHXP0uRi/ARlHQ+GNp6foYIsevC0OtLdau0/ouFlfGkEep
3aIV5oECgYEAyyWrNhw+BhNFXsyPzEQ4/mO5ucup3cE/tAAtLiSckoXjmY8K7PQr
xfKRCkuM1qpcxtYkbTs35aOdK48gL0NVd50QzrWFrQkQkVnpnJ1lYeVgEL1DmalD
B55bwTdShcs0gEoKefZCvmotrmYdSpMGsapqqbZFrysFFzRDyDxnHfcCgYEAxVjA
/dXxCEUjYFVC3i833lI/yiycJrhjIeffc6DqpSReuTU+i8Nh3sLiytaSqPFVASDS
08K3JwVguMTzDgrYkl365lm50WxcBuNgLkSqA90vE/H6gkRZVkuzOb7T+ZdDxf0s
7RH4aqeeOSiOcZ3uC+d53UArJFidETXbgguXkAMCgYA22Ynbx05b15IwYW0mCvmU
fhqkdr/7lvT7RdztC4eW7D2itYOOrPKwtKjCrdluEHuSWDlnoMib4UxLeY6IFFcc
P7VNCqf4K21kwXEZD0pTX1pLyr5Y2+G0SeaeSbCnXVFknhksCvjEbui8oOehvgbd
q5S3E/bGsAfk1wDCLMTuywKBgACHrH0CBhOvm9i2YeeW2N+P+PviAslX1WxR4xe8
ZuTqpBZ7Ph/B9pFSlKlWyi4J9+B45hgLfdJtAUV9welXvh0mg3X657TYRab/FVMK
fCpmfangDHwtEtBYg7K0AH27GkN92pEIa1JeAN7GbRuBARKnHHyrn3IJiuJw8pX2
0gFhAoGBAIquI9sAB2dKEOMW+iQJkLH8Hh8/EWyslow+QJiyIsRe1l9jtkOxC5D3
Hj4yO4j5LOWDMTgDcLsZTxbGiTzkNc/HghrNIevDAQdgjJQNl84zDjyyCA4r/MA7
bYJTtYj8q6J0EDbRdT9b6hMclyzjNXdx2loJxR0R8WUeL1lDEPq8
-----END RSA PRIVATE KEY-----`
// 测试token生成
func TestGenerateToken(t *testing.T) {
jwtService := NewJwt(pk, time.Second*1000)
token := jwtService.GenerateToken(1)
if token == "" {
t.Fatal("token生成失败")
}
fmt.Println(pk, token)
}
// 测试token解析
func TestParseToken(t *testing.T) {
jwtService := NewJwt(pk, time.Second*1000)
token := jwtService.GenerateToken(999)
if token == "" {
t.Fatal("token生成失败")
}
uid, err := jwtService.ParseToken(token)
if err != nil {
t.Fatal("token解析失败", err)
}
if uid != 999 {
t.Fatal("token解析失败")
}
}
func BenchmarkJwtService_GenerateToken(b *testing.B) {
jwtService := NewJwt(pk, time.Second*1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
jwtService.GenerateToken(999)
}
}
func BenchmarkJwtService_ParseToken(b *testing.B) {
jwtService := NewJwt(pk, time.Second*1000)
token := jwtService.GenerateToken(999)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = jwtService.ParseToken(token)
}
}

32
lib/lock/local.go Normal file
View File

@@ -0,0 +1,32 @@
package lock
import (
"sync"
)
type Local struct {
Locks *sync.Map
}
func (l *Local) Lock(key string) {
lock := l.GetLock(key)
lock.Lock()
}
func (l *Local) UnLock(key string) {
lock, ok := l.Locks.Load(key)
if ok {
lock.(*sync.Mutex).Unlock()
}
}
func (l *Local) GetLock(key string) *sync.Mutex {
lock, _ := l.Locks.LoadOrStore(key, &sync.Mutex{})
return lock.(*sync.Mutex)
}
func NewLocal() *Local {
return &Local{
Locks: &sync.Map{},
}
}

100
lib/lock/local_test.go Normal file
View File

@@ -0,0 +1,100 @@
package lock
import (
"fmt"
"sync"
"testing"
)
func TestLocal_GetLock(t *testing.T) {
l := NewLocal()
wg := sync.WaitGroup{}
wg.Add(3)
var l1 *sync.Mutex
var l2 *sync.Mutex
var l3 *sync.Mutex
i := 0
go func() {
l1 = l.GetLock("key")
fmt.Println("l1", l1, i)
l1.Lock()
fmt.Println("l1", i)
i++
l1.Unlock()
wg.Done()
}()
go func() {
l2 = l.GetLock("key")
fmt.Println("l2", l2, i)
l2.Lock()
fmt.Println("l2", i)
i++
l2.Unlock()
wg.Done()
}()
go func() {
l3 = l.GetLock("key")
fmt.Println("l3", l3, i)
l3.Lock()
fmt.Println("l3", i)
i++
l3.Unlock()
wg.Done()
}()
wg.Wait()
fmt.Println(l1, l2, l3)
fmt.Println(l1 == l2, l2 == l3)
fmt.Println(&sync.Mutex{} == &sync.Mutex{})
}
func TestLocal_Lock(t *testing.T) {
l := NewLocal()
wg := sync.WaitGroup{}
wg.Add(3)
i := 0
go func() {
l.Lock("key")
fmt.Println("l1", i)
i++
l.UnLock("key")
wg.Done()
}()
go func() {
l.Lock("key")
fmt.Println("l2", i)
i++
l.UnLock("key")
wg.Done()
}()
go func() {
l.Lock("key")
fmt.Println("l3", i)
i++
l.UnLock("key")
wg.Done()
}()
wg.Wait()
}
func TestSyncMap(t *testing.T) {
m := sync.Map{}
wg := sync.WaitGroup{}
wg.Add(3)
go func() {
v, ok := m.LoadOrStore("key", 1)
fmt.Println(1, v, ok)
wg.Done()
}()
go func() {
v, ok := m.LoadOrStore("key", 2)
fmt.Println(2, v, ok)
wg.Done()
}()
go func() {
v, ok := m.LoadOrStore("key", 3)
fmt.Println(3, v, ok)
wg.Done()
}()
wg.Wait()
}

9
lib/lock/lock.go Normal file
View File

@@ -0,0 +1,9 @@
package lock
import "sync"
type Locker interface {
GetLock(key string) *sync.Mutex
Lock(key string)
UnLock(key string)
}

54
lib/logger/logger.go Normal file
View File

@@ -0,0 +1,54 @@
package logger
import (
nested "github.com/antonfisher/nested-logrus-formatter"
log "github.com/sirupsen/logrus"
"io"
"os"
)
const (
DebugMode = "debug"
ReleaseMode = "release"
)
type Config struct {
Path string
Level string
ReportCaller bool
}
func New(c *Config) *log.Logger {
log.SetFormatter(&nested.Formatter{
// HideKeys: true,
TimestampFormat: "2006-01-02 15:04:05",
NoColors: true,
NoFieldsColors: true,
//FieldsOrder: []string{"name", "age"},
})
// 日志文件
f := c.Path
var write io.Writer
if f != "" {
fwriter, err := os.OpenFile(f, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
panic("open log file fail!")
}
write = io.MultiWriter(fwriter, os.Stdout)
} else {
write = os.Stdout
}
log.SetOutput(write)
log.SetReportCaller(c.ReportCaller)
level, err2 := log.ParseLevel(c.Level)
if err2 != nil {
level = log.DebugLevel
}
log.SetLevel(level)
return log.StandardLogger()
}

40
lib/orm/mysql.go Normal file
View File

@@ -0,0 +1,40 @@
package orm
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type MysqlConfig struct {
Dns string
MaxIdleConns int
MaxOpenConns int
}
func NewMysql(mysqlConf *MysqlConfig) *gorm.DB {
db, err := gorm.Open(mysql.New(mysql.Config{
DSN: mysqlConf.Dns, // DSN data source name
DefaultStringSize: 256, // string 类型字段的默认长度
//DisableDatetimePrecision: true, // 禁用 datetime 精度MySQL 5.6 之前的数据库不支持
//DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
//DontSupportRenameColumn: true, // 用 `change` 重命名列MySQL 8 之前的数据库和 MariaDB 不支持重命名列
//SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置
}), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
})
if err != nil {
fmt.Println(err)
}
sqlDB, err2 := db.DB()
if err2 != nil {
fmt.Println(err2)
}
// SetMaxIdleConns 设置空闲连接池中连接的最大数量
sqlDB.SetMaxIdleConns(mysqlConf.MaxIdleConns)
// SetMaxOpenConns 设置打开数据库连接的最大数量。
sqlDB.SetMaxOpenConns(mysqlConf.MaxOpenConns)
return db
}

30
lib/orm/sqlite.go Normal file
View File

@@ -0,0 +1,30 @@
package orm
import (
"fmt"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type SqliteConfig struct {
MaxIdleConns int
MaxOpenConns int
}
func NewSqlite(sqliteConf *SqliteConfig) *gorm.DB {
db, err := gorm.Open(sqlite.Open("./data/rustdeskapi.db"), &gorm.Config{})
if err != nil {
fmt.Println(err)
}
sqlDB, err2 := db.DB()
if err2 != nil {
fmt.Println(err2)
}
// SetMaxIdleConns 设置空闲连接池中连接的最大数量
sqlDB.SetMaxIdleConns(sqliteConf.MaxIdleConns)
// SetMaxOpenConns 设置打开数据库连接的最大数量。
sqlDB.SetMaxOpenConns(sqliteConf.MaxOpenConns)
return db
}

4
lib/upload/local.go Normal file
View File

@@ -0,0 +1,4 @@
package upload
type Local struct {
}

475
lib/upload/oss.go Normal file
View File

@@ -0,0 +1,475 @@
package upload
import (
"bytes"
"crypto"
"crypto/hmac"
"crypto/md5"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"hash"
"io"
"io/ioutil"
"net/http"
"strconv"
"time"
)
type Oss struct {
AccessKeyId string
AccessKeySecret string
Host string
CallbackUrl string
ExpireTime int64
MaxByte int64
}
const (
base64Table = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"
)
var coder = base64.NewEncoding(base64Table)
func base64Encode(src []byte) []byte {
return []byte(coder.EncodeToString(src))
}
func get_gmt_iso8601(expire_end int64) string {
var tokenExpire = time.Unix(expire_end, 0).UTC().Format("2006-01-02T15:04:05Z")
return tokenExpire
}
type ConfigStruct struct {
Expiration string `json:"expiration"`
Conditions [][]interface{} `json:"conditions"`
}
type PolicyToken struct {
AccessKeyId string `json:"accessid"`
Host string `json:"host"`
Expire int64 `json:"expire"`
Signature string `json:"signature"`
Policy string `json:"policy"`
Directory string `json:"dir"`
Callback string `json:"callback"`
}
type CallbackParam struct {
CallbackUrl string `json:"callbackUrl"`
CallbackBody string `json:"callbackBody"`
CallbackBodyType string `json:"callbackBodyType"`
}
type CallbackBaseForm struct {
Bucket string `json:"bucket" form:"bucket"`
Etag string `json:"etag" form:"etag"`
Filename string `json:"filename" form:"filename"`
Size string `json:"size" form:"size"`
MimeType string `json:"mime_type" form:"mime_type"`
Height string `json:"height" form:"height"`
Width string `json:"width" form:"width"`
Format string `json:"format" form:"format"`
OriginFilename string `json:"origin_filename" form:"origin_filename"`
}
func (oc *Oss) GetPolicyToken(uploadDir string) string {
now := time.Now().Unix()
expire_end := now + oc.ExpireTime
var tokenExpire = get_gmt_iso8601(expire_end)
//create post policy json
var config ConfigStruct
config.Expiration = tokenExpire
var condition = []interface{}{"starts-with", "$key", uploadDir}
var condition_limit = []interface{}{"content-length-range", 0, oc.MaxByte}
config.Conditions = append(config.Conditions, condition, condition_limit)
//calucate signature
result, err := json.Marshal(config)
debyte := base64.StdEncoding.EncodeToString(result)
h := hmac.New(func() hash.Hash {
return sha1.New()
}, []byte(oc.AccessKeySecret))
io.WriteString(h, debyte)
signedStr := base64.StdEncoding.EncodeToString(h.Sum(nil))
var callbackParam CallbackParam
callbackParam.CallbackUrl = oc.CallbackUrl
callbackParam.CallbackBody =
"bucket=${bucket}&" +
"etag=${etag}&" +
"filename=${object}&" +
"size=${size}&" +
"mime_type=${mimeType}&" +
"height=${imageInfo.height}&" +
"width=${imageInfo.width}&" +
"format=${imageInfo.format}&" +
"origin_filename=${x:origin_filename}"
callbackParam.CallbackBodyType = "application/x-www-form-urlencoded"
callback_str, err := json.Marshal(callbackParam)
if err != nil {
fmt.Println("callback json err:", err)
}
callbackBase64 := base64.StdEncoding.EncodeToString(callback_str)
var policyToken PolicyToken
policyToken.AccessKeyId = oc.AccessKeyId
policyToken.Host = oc.Host
policyToken.Expire = expire_end
policyToken.Signature = string(signedStr)
policyToken.Directory = uploadDir
policyToken.Policy = string(debyte)
policyToken.Callback = string(callbackBase64)
response, err := json.Marshal(policyToken)
if err != nil {
fmt.Println("json err:", err)
}
return string(response)
}
func (oc *Oss) Verify(r *http.Request) bool {
// Get PublicKey bytes
bytePublicKey, err := getPublicKey(r)
if err != nil {
return false
}
// Get Authorization bytes : decode from Base64String
byteAuthorization, err := getAuthorization(r)
if err != nil {
return false
}
// Get MD5 bytes from Newly Constructed Authrization String.
byteMD5, err := getMD5FromNewAuthString(r)
if err != nil {
return false
}
// verifySignature and response to client
if verifySignature(bytePublicKey, byteMD5, byteAuthorization) {
// do something you want accoding to callback_body ...
return true
} else {
return false
}
}
// getPublicKey : Get PublicKey bytes from Request.URL
func getPublicKey(r *http.Request) ([]byte, error) {
var bytePublicKey []byte
// get PublicKey URL
publicKeyURLBase64 := r.Header.Get("x-oss-pub-key-url")
if publicKeyURLBase64 == "" {
fmt.Println("GetPublicKey from Request header failed : No x-oss-pub-key-url field. ")
return bytePublicKey, errors.New("no x-oss-pub-key-url field in Request header ")
}
publicKeyURL, _ := base64.StdEncoding.DecodeString(publicKeyURLBase64)
// fmt.Printf("publicKeyURL={%s}\n", publicKeyURL)
// get PublicKey Content from URL
responsePublicKeyURL, err := http.Get(string(publicKeyURL))
if err != nil {
fmt.Printf("Get PublicKey Content from URL failed : %s \n", err.Error())
return bytePublicKey, err
}
bytePublicKey, err = ioutil.ReadAll(responsePublicKeyURL.Body)
if err != nil {
fmt.Printf("Read PublicKey Content from URL failed : %s \n", err.Error())
return bytePublicKey, err
}
defer responsePublicKeyURL.Body.Close()
// fmt.Printf("publicKey={%s}\n", bytePublicKey)
return bytePublicKey, nil
}
// getAuthorization : decode from Base64String
func getAuthorization(r *http.Request) ([]byte, error) {
var byteAuthorization []byte
// Get Authorization bytes : decode from Base64String
strAuthorizationBase64 := r.Header.Get("authorization")
if strAuthorizationBase64 == "" {
fmt.Println("Failed to get authorization field from request header. ")
return byteAuthorization, errors.New("no authorization field in Request header")
}
byteAuthorization, _ = base64.StdEncoding.DecodeString(strAuthorizationBase64)
return byteAuthorization, nil
}
// getMD5FromNewAuthString : Get MD5 bytes from Newly Constructed Authrization String.
func getMD5FromNewAuthString(r *http.Request) ([]byte, error) {
var byteMD5 []byte
// Construct the New Auth String from URI+Query+Body
bodyContent, err := ioutil.ReadAll(r.Body)
r.Body.Close()
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyContent))
if err != nil {
fmt.Printf("Read Request Body failed : %s \n", err.Error())
return byteMD5, err
}
strCallbackBody := string(bodyContent)
// fmt.Printf("r.URL.RawPath={%s}, r.URL.Query()={%s}, strCallbackBody={%s}\n", r.URL.RawPath, r.URL.Query(), strCallbackBody)
strURLPathDecode, errUnescape := unescapePath(r.URL.Path, encodePathSegment) //url.PathUnescape(r.URL.Path) for Golang v1.8.2+
if errUnescape != nil {
fmt.Printf("url.PathUnescape failed : URL.Path=%s, error=%s \n", r.URL.Path, err.Error())
return byteMD5, errUnescape
}
// Generate New Auth String prepare for MD5
strAuth := ""
if r.URL.RawQuery == "" {
strAuth = fmt.Sprintf("%s\n%s", strURLPathDecode, strCallbackBody)
} else {
strAuth = fmt.Sprintf("%s?%s\n%s", strURLPathDecode, r.URL.RawQuery, strCallbackBody)
}
// fmt.Printf("NewlyConstructedAuthString={%s}\n", strAuth)
// Generate MD5 from the New Auth String
md5Ctx := md5.New()
md5Ctx.Write([]byte(strAuth))
byteMD5 = md5Ctx.Sum(nil)
return byteMD5, nil
}
/* VerifySignature
* VerifySignature需要三个重要的数据信息来进行签名验证 1>获取公钥PublicKey; 2>生成新的MD5鉴权串; 3>解码Request携带的鉴权串;
* 1>获取公钥PublicKey : 从RequestHeader的"x-oss-pub-key-url"字段中获取 URL, 读取URL链接的包含的公钥内容 进行解码解析, 将其作为rsa.VerifyPKCS1v15的入参。
* 2>生成新的MD5鉴权串 : 把Request中的url中的path部分进行urldecode 加上url的query部分 再加上body 组合之后进行MD5编码 得到MD5鉴权字节串。
* 3>解码Request携带的鉴权串 获取RequestHeader的"authorization"字段, 对其进行Base64解码作为签名验证的鉴权对比串。
* rsa.VerifyPKCS1v15进行签名验证返回验证结果。
* */
func verifySignature(bytePublicKey []byte, byteMd5 []byte, authorization []byte) bool {
pubBlock, _ := pem.Decode(bytePublicKey)
if pubBlock == nil {
fmt.Printf("Failed to parse PEM block containing the public key")
return false
}
pubInterface, err := x509.ParsePKIXPublicKey(pubBlock.Bytes)
if (pubInterface == nil) || (err != nil) {
fmt.Printf("x509.ParsePKIXPublicKey(publicKey) failed : %s \n", err.Error())
return false
}
pub := pubInterface.(*rsa.PublicKey)
errorVerifyPKCS1v15 := rsa.VerifyPKCS1v15(pub, crypto.MD5, byteMd5, authorization)
if errorVerifyPKCS1v15 != nil {
fmt.Printf("\nSignature Verification is Failed : %s \n", errorVerifyPKCS1v15.Error())
//printByteArray(byteMd5, "AuthMd5(fromNewAuthString)")
//printByteArray(bytePublicKey, "PublicKeyBase64")
//printByteArray(authorization, "AuthorizationFromRequest")
return false
}
fmt.Printf("\nSignature Verification is Successful. \n")
return true
}
func printByteArray(byteArrary []byte, arrName string) {
fmt.Printf("++++++++ printByteArray : ArrayName=%s, ArrayLength=%d \n", arrName, len(byteArrary))
for i := 0; i < len(byteArrary); i++ {
fmt.Printf("%02x", byteArrary[i])
}
fmt.Printf("\n-------- printByteArray : End . \n")
}
type EscapeError string
func (e EscapeError) Error() string {
return "invalid URL escape " + strconv.Quote(string(e))
}
type InvalidHostError string
func (e InvalidHostError) Error() string {
return "invalid character " + strconv.Quote(string(e)) + " in host name"
}
type encoding int
const (
encodePath encoding = 1 + iota
encodePathSegment
encodeHost
encodeZone
encodeUserPassword
encodeQueryComponent
encodeFragment
)
// unescapePath : unescapes a string; the mode specifies, which section of the URL string is being unescaped.
func unescapePath(s string, mode encoding) (string, error) {
// Count %, check that they're well-formed.
mode = encodePathSegment
n := 0
hasPlus := false
for i := 0; i < len(s); {
switch s[i] {
case '%':
n++
if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) {
s = s[i:]
if len(s) > 3 {
s = s[:3]
}
return "", EscapeError(s)
}
// Per https://tools.ietf.org/html/rfc3986#page-21
// in the host component %-encoding can only be used
// for non-ASCII bytes.
// But https://tools.ietf.org/html/rfc6874#section-2
// introduces %25 being allowed to escape a percent sign
// in IPv6 scoped-address literals. Yay.
if mode == encodeHost && unhex(s[i+1]) < 8 && s[i:i+3] != "%25" {
return "", EscapeError(s[i : i+3])
}
if mode == encodeZone {
// RFC 6874 says basically "anything goes" for zone identifiers
// and that even non-ASCII can be redundantly escaped,
// but it seems prudent to restrict %-escaped bytes here to those
// that are valid host name bytes in their unescaped form.
// That is, you can use escaping in the zone identifier but not
// to introduce bytes you couldn't just write directly.
// But Windows puts spaces here! Yay.
v := unhex(s[i+1])<<4 | unhex(s[i+2])
if s[i:i+3] != "%25" && v != ' ' && shouldEscape(v, encodeHost) {
return "", EscapeError(s[i : i+3])
}
}
i += 3
case '+':
hasPlus = mode == encodeQueryComponent
i++
default:
if (mode == encodeHost || mode == encodeZone) && s[i] < 0x80 && shouldEscape(s[i], mode) {
return "", InvalidHostError(s[i : i+1])
}
i++
}
}
if n == 0 && !hasPlus {
return s, nil
}
t := make([]byte, len(s)-2*n)
j := 0
for i := 0; i < len(s); {
switch s[i] {
case '%':
t[j] = unhex(s[i+1])<<4 | unhex(s[i+2])
j++
i += 3
case '+':
if mode == encodeQueryComponent {
t[j] = ' '
} else {
t[j] = '+'
}
j++
i++
default:
t[j] = s[i]
j++
i++
}
}
return string(t), nil
}
// Please be informed that for now shouldEscape does not check all
// reserved characters correctly. See golang.org/issue/5684.
func shouldEscape(c byte, mode encoding) bool {
// §2.3 Unreserved characters (alphanum)
if 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' {
return false
}
if mode == encodeHost || mode == encodeZone {
// §3.2.2 Host allows
// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
// as part of reg-name.
// We add : because we include :port as part of host.
// We add [ ] because we include [ipv6]:port as part of host.
// We add < > because they're the only characters left that
// we could possibly allow, and Parse will reject them if we
// escape them (because hosts can't use %-encoding for
// ASCII bytes).
switch c {
case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '[', ']', '<', '>', '"':
return false
}
}
switch c {
case '-', '_', '.', '~': // §2.3 Unreserved characters (mark)
return false
case '$', '&', '+', ',', '/', ':', ';', '=', '?', '@': // §2.2 Reserved characters (reserved)
// Different sections of the URL allow a few of
// the reserved characters to appear unescaped.
switch mode {
case encodePath: // §3.3
// The RFC allows : @ & = + $ but saves / ; , for assigning
// meaning to individual path segments. This package
// only manipulates the path as a whole, so we allow those
// last three as well. That leaves only ? to escape.
return c == '?'
case encodePathSegment: // §3.3
// The RFC allows : @ & = + $ but saves / ; , for assigning
// meaning to individual path segments.
return c == '/' || c == ';' || c == ',' || c == '?'
case encodeUserPassword: // §3.2.1
// The RFC allows ';', ':', '&', '=', '+', '$', and ',' in
// userinfo, so we must escape only '@', '/', and '?'.
// The parsing of userinfo treats ':' as special so we must escape
// that too.
return c == '@' || c == '/' || c == '?' || c == ':'
case encodeQueryComponent: // §3.4
// The RFC reserves (so we must escape) everything.
return true
case encodeFragment: // §4.1
// The RFC text is silent but the grammar allows
// everything, so escape nothing.
return false
}
}
// Everything else must be escaped.
return true
}
func ishex(c byte) bool {
switch {
case '0' <= c && c <= '9':
return true
case 'a' <= c && c <= 'f':
return true
case 'A' <= c && c <= 'F':
return true
}
return false
}
func unhex(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 0
}

44
model/addressBook.go Normal file
View File

@@ -0,0 +1,44 @@
package model
import "Gwen/model/custom_types"
// final String id;
// String hash; // personal ab hash password
// String password; // shared ab password
// String username; // pc username
// String hostname;
// String platform;
// String alias;
// List<dynamic> tags;
// bool forceAlwaysRelay = false;
// String rdpPort;
// String rdpUsername;
// bool online = false;
// String loginName; //login username
// bool? sameServer;
// AddressBook 有些字段是Personal才会上传的
type AddressBook struct {
RowId uint `gorm:"primaryKey" json:"row_id"`
Id string `json:"id" gorm:"default:0;not null;index"`
Username string `json:"username" gorm:"default:'';not null;"`
Password string `json:"password" gorm:"default:'';not null;"`
Hostname string `json:"hostname" gorm:"default:'';not null;"`
Alias string `json:"alias" gorm:"default:'';not null;"`
Platform string `json:"platform" gorm:"default:'';not null;"`
Tags custom_types.AutoJson `json:"tags" gorm:"not null;" swaggertype:"array,string"`
Hash string `json:"hash" gorm:"default:'';not null;"`
UserId uint `json:"user_id" gorm:"default:0;not null;index"`
ForceAlwaysRelay bool `json:"forceAlwaysRelay" gorm:"default:0;not null;"`
RdpPort string `json:"rdpPort" gorm:"default:'';not null;"`
RdpUsername string `json:"rdpUsername" gorm:"default:'';not null;"`
Online bool `json:"online" gorm:"default:0;not null;"`
LoginName string `json:"loginName" gorm:"default:'';not null;"`
SameServer bool `json:"sameServer" gorm:"default:0;not null;"`
TimeModel
}
type AddressBookList struct {
AddressBooks []*AddressBook `json:"list"`
Pagination
}

View File

@@ -0,0 +1,66 @@
package custom_types
import (
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
)
// AutoJson 数据类型
type AutoJson json.RawMessage
func (j *AutoJson) Scan(value interface{}) error {
var strValue string
switch v := value.(type) {
case []byte:
strValue = string(v)
case string:
strValue = v
default:
return errors.New(fmt.Sprintf("Failed Scan AutoJson value: %v", value))
}
bytes := []byte(strValue)
//bytes, ok := value.([]byte)
//if !ok {
// return errors.New(fmt.Sprint("Failed Scan AutoJson value:", value))
//}
if bytes == nil || len(bytes) == 0 {
*j = AutoJson(json.RawMessage{'[', ']'})
return nil
}
result := &json.RawMessage{}
err := json.Unmarshal(bytes, result)
//解析json错误 返回空
if err != nil {
*j = AutoJson(json.RawMessage{'[', ']'})
return nil
}
*j = AutoJson(*result)
return err
}
func (j AutoJson) Value() (driver.Value, error) {
bytes, err := json.RawMessage(j).MarshalJSON()
return string(bytes), err
}
func (j AutoJson) MarshalJSON() ([]byte, error) {
b, err := json.RawMessage(j).MarshalJSON()
if err != nil {
return nil, err
}
return b, err
}
func (j *AutoJson) UnmarshalJSON(b []byte) error {
result := json.RawMessage{}
err := result.UnmarshalJSON(b)
*j = AutoJson(result)
return err
}
func (j AutoJson) String() string {
s, _ := j.MarshalJSON()
return (string)(s)
}

View File

@@ -0,0 +1,24 @@
package custom_types
import (
"database/sql/driver"
"time"
)
// AutoTime 自定义时间格式
type AutoTime time.Time
func (mt AutoTime) Value() (driver.Value, error) {
var zeroTime time.Time
t := time.Time(mt)
if t.UnixNano() == zeroTime.UnixNano() {
return nil, nil
}
return t, nil
}
func (mt AutoTime) MarshalJSON() ([]byte, error) {
//b := make([]byte, 0, len("2006-01-02 15:04:05")+2)
b := time.Time(mt).AppendFormat([]byte{}, "\"2006-01-02 15:04:05\"")
return b, nil
}

18
model/group.go Normal file
View File

@@ -0,0 +1,18 @@
package model
const (
GroupTypeDefault = 1 // 默认
GroupTypeShare = 2 // 共享
)
type Group struct {
IdModel
Name string `json:"name" gorm:"default:'';not null;"`
Type int `json:"type" gorm:"default:1;not null;"`
TimeModel
}
type GroupList struct {
Groups []*Group `json:"list"`
Pagination
}

27
model/model.go Normal file
View File

@@ -0,0 +1,27 @@
package model
import (
"Gwen/model/custom_types"
)
type StatusCode int
const (
COMMON_STATUS_ENABLE StatusCode = 1 //通用状态 启用
COMMON_STATUS_DISABLED StatusCode = 2 //通用状态 禁用
)
type IdModel struct {
Id uint `gorm:"primaryKey" json:"id"`
}
type TimeModel struct {
CreatedAt custom_types.AutoTime `json:"created_at" gorm:"type:timestamp;"`
UpdatedAt custom_types.AutoTime `json:"updated_at" gorm:"type:timestamp;"`
}
// Pagination
type Pagination struct {
Page int64 `form:"page" json:"page"`
Total int64 `form:"total" json:"total"`
PageSize int64 `form:"page_size" json:"page_size"`
}

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