From c53df223d1232f548f449d230b5c3b2970c7e6f2 Mon Sep 17 00:00:00 2001 From: ljw <84855512@qq.com> Date: Fri, 13 Sep 2024 15:57:29 +0800 Subject: [PATCH] first --- .gitignore | 9 + README.md | 105 ++ build.bat | 14 + build.sh | 15 + cmd/apimain.go | 252 +++ conf/config.yaml | 42 + conf/jwt_pri.pem | 0 config/cache.go | 9 + config/config.go | 56 + config/gin.go | 8 + config/gorm.go | 19 + config/jwt.go | 8 + config/logger.go | 7 + config/oss.go | 10 + config/redis.go | 7 + config/rustdesk.go | 8 + docs/admin/admin_docs.go | 2331 ++++++++++++++++++++++++++ docs/admin/admin_swagger.json | 2306 +++++++++++++++++++++++++ docs/admin/admin_swagger.yaml | 1411 ++++++++++++++++ docs/api/api_docs.go | 830 +++++++++ docs/api/api_swagger.json | 805 +++++++++ docs/api/api_swagger.yaml | 513 ++++++ docs/api_swag.png | Bin 0 -> 56623 bytes docs/pc_ab.png | Bin 0 -> 54795 bytes docs/pc_gr.png | Bin 0 -> 54764 bytes docs/web_admin.png | Bin 0 -> 45140 bytes docs/web_admin_gr.png | Bin 0 -> 7921 bytes docs/web_resetpwd.png | Bin 0 -> 3710 bytes docs/web_user.png | Bin 0 -> 27555 bytes docs/webclient_conf.png | Bin 0 -> 22999 bytes generate_api.go | 5 + global/global.go | 33 + go.mod | 73 + http/controller/admin/addressBook.go | 191 +++ http/controller/admin/file.go | 83 + http/controller/admin/group.go | 160 ++ http/controller/admin/login.go | 74 + http/controller/admin/peer.go | 160 ++ http/controller/admin/rustdesk.go | 30 + http/controller/admin/tag.go | 190 +++ http/controller/admin/user.go | 261 +++ http/controller/api/ab.go | 150 ++ http/controller/api/group.go | 115 ++ http/controller/api/index.go | 39 + http/controller/api/login.go | 90 + http/controller/api/peer.go | 48 + http/controller/api/user.go | 74 + http/controller/api/webClient.go | 42 + http/controller/web/index.go | 68 + http/http.go | 30 + http/middleware/admin.go | 32 + http/middleware/admin_privilege.go | 22 + http/middleware/cors.go | 23 + http/middleware/jwt.go | 50 + http/middleware/logger.go | 20 + http/middleware/rustauth.go | 44 + http/request/admin/addressBook.go | 56 + http/request/admin/group.go | 21 + http/request/admin/login.go | 6 + http/request/admin/peer.go | 30 + http/request/admin/tag.go | 33 + http/request/admin/user.go | 57 + http/request/api/peer.go | 37 + http/request/api/user.go | 41 + http/response/admin/user.go | 13 + http/response/api/ab.go | 9 + http/response/api/peer.go | 74 + http/response/api/user.go | 55 + http/response/api/webClient.go | 55 + http/response/response.go | 53 + http/router/admin.go | 118 ++ http/router/api.go | 73 + http/router/router.go | 15 + http/run.go | 12 + http/run_win.go | 11 + lib/cache/cache.go | 71 + lib/cache/cache_test.go | 92 + lib/cache/file.go | 103 ++ lib/cache/file_test.go | 94 ++ lib/cache/memory.go | 215 +++ lib/cache/memory_test.go | 107 ++ lib/cache/redis.go | 49 + lib/cache/redis_test.go | 94 ++ lib/cache/simple_cache.go | 65 + lib/cache/simple_cache_test.go | 108 ++ lib/jwt/jwt.go | 61 + lib/jwt/jwt_test.go | 80 + lib/lock/local.go | 32 + lib/lock/local_test.go | 100 ++ lib/lock/lock.go | 9 + lib/logger/logger.go | 54 + lib/orm/mysql.go | 40 + lib/orm/sqlite.go | 30 + lib/upload/local.go | 4 + lib/upload/oss.go | 475 ++++++ model/addressBook.go | 44 + model/custom_types/auto_json.go | 66 + model/custom_types/auto_time.go | 24 + model/group.go | 18 + model/model.go | 27 + model/peer.go | 21 + model/tag.go | 14 + model/user.go | 18 + model/userToken.go | 9 + model/version.go | 7 + service/addressBook.go | 109 ++ service/group.go | 45 + service/peer.go | 63 + service/service.go | 42 + service/tag.go | 88 + service/user.go | 171 ++ utils/tools.go | 63 + 112 files changed, 14353 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.bat create mode 100644 build.sh create mode 100644 cmd/apimain.go create mode 100644 conf/config.yaml create mode 100644 conf/jwt_pri.pem create mode 100644 config/cache.go create mode 100644 config/config.go create mode 100644 config/gin.go create mode 100644 config/gorm.go create mode 100644 config/jwt.go create mode 100644 config/logger.go create mode 100644 config/oss.go create mode 100644 config/redis.go create mode 100644 config/rustdesk.go create mode 100644 docs/admin/admin_docs.go create mode 100644 docs/admin/admin_swagger.json create mode 100644 docs/admin/admin_swagger.yaml create mode 100644 docs/api/api_docs.go create mode 100644 docs/api/api_swagger.json create mode 100644 docs/api/api_swagger.yaml create mode 100644 docs/api_swag.png create mode 100644 docs/pc_ab.png create mode 100644 docs/pc_gr.png create mode 100644 docs/web_admin.png create mode 100644 docs/web_admin_gr.png create mode 100644 docs/web_resetpwd.png create mode 100644 docs/web_user.png create mode 100644 docs/webclient_conf.png create mode 100644 generate_api.go create mode 100644 global/global.go create mode 100644 go.mod create mode 100644 http/controller/admin/addressBook.go create mode 100644 http/controller/admin/file.go create mode 100644 http/controller/admin/group.go create mode 100644 http/controller/admin/login.go create mode 100644 http/controller/admin/peer.go create mode 100644 http/controller/admin/rustdesk.go create mode 100644 http/controller/admin/tag.go create mode 100644 http/controller/admin/user.go create mode 100644 http/controller/api/ab.go create mode 100644 http/controller/api/group.go create mode 100644 http/controller/api/index.go create mode 100644 http/controller/api/login.go create mode 100644 http/controller/api/peer.go create mode 100644 http/controller/api/user.go create mode 100644 http/controller/api/webClient.go create mode 100644 http/controller/web/index.go create mode 100644 http/http.go create mode 100644 http/middleware/admin.go create mode 100644 http/middleware/admin_privilege.go create mode 100644 http/middleware/cors.go create mode 100644 http/middleware/jwt.go create mode 100644 http/middleware/logger.go create mode 100644 http/middleware/rustauth.go create mode 100644 http/request/admin/addressBook.go create mode 100644 http/request/admin/group.go create mode 100644 http/request/admin/login.go create mode 100644 http/request/admin/peer.go create mode 100644 http/request/admin/tag.go create mode 100644 http/request/admin/user.go create mode 100644 http/request/api/peer.go create mode 100644 http/request/api/user.go create mode 100644 http/response/admin/user.go create mode 100644 http/response/api/ab.go create mode 100644 http/response/api/peer.go create mode 100644 http/response/api/user.go create mode 100644 http/response/api/webClient.go create mode 100644 http/response/response.go create mode 100644 http/router/admin.go create mode 100644 http/router/api.go create mode 100644 http/router/router.go create mode 100644 http/run.go create mode 100644 http/run_win.go create mode 100644 lib/cache/cache.go create mode 100644 lib/cache/cache_test.go create mode 100644 lib/cache/file.go create mode 100644 lib/cache/file_test.go create mode 100644 lib/cache/memory.go create mode 100644 lib/cache/memory_test.go create mode 100644 lib/cache/redis.go create mode 100644 lib/cache/redis_test.go create mode 100644 lib/cache/simple_cache.go create mode 100644 lib/cache/simple_cache_test.go create mode 100644 lib/jwt/jwt.go create mode 100644 lib/jwt/jwt_test.go create mode 100644 lib/lock/local.go create mode 100644 lib/lock/local_test.go create mode 100644 lib/lock/lock.go create mode 100644 lib/logger/logger.go create mode 100644 lib/orm/mysql.go create mode 100644 lib/orm/sqlite.go create mode 100644 lib/upload/local.go create mode 100644 lib/upload/oss.go create mode 100644 model/addressBook.go create mode 100644 model/custom_types/auto_json.go create mode 100644 model/custom_types/auto_time.go create mode 100644 model/group.go create mode 100644 model/model.go create mode 100644 model/peer.go create mode 100644 model/tag.go create mode 100644 model/user.go create mode 100644 model/userToken.go create mode 100644 model/version.go create mode 100644 service/addressBook.go create mode 100644 service/group.go create mode 100644 service/peer.go create mode 100644 service/service.go create mode 100644 service/tag.go create mode 100644 service/user.go create mode 100644 utils/tools.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d03aeac --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.idea +runtime +!runtime/cache/.gitignore +go.sum +resources +!resources/public/upload/.gitignore +!resources/public/web +release +data \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d2a0865 --- /dev/null +++ b/README.md @@ -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 -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. 后台文档 /admin/swagger/index.html +2. PC端文档 /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` + 目录下生成对应的可执行文件。 diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..0928c96 --- /dev/null +++ b/build.bat @@ -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 diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..f4876b2 --- /dev/null +++ b/build.sh @@ -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/ diff --git a/cmd/apimain.go b/cmd/apimain.go new file mode 100644 index 0000000..2ba29f2 --- /dev/null +++ b/cmd/apimain.go @@ -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) + } + +} diff --git a/conf/config.yaml b/conf/config.yaml new file mode 100644 index 0000000..2c52163 --- /dev/null +++ b/conf/config.yaml @@ -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 \ No newline at end of file diff --git a/conf/jwt_pri.pem b/conf/jwt_pri.pem new file mode 100644 index 0000000..e69de29 diff --git a/config/cache.go b/config/cache.go new file mode 100644 index 0000000..1485e11 --- /dev/null +++ b/config/cache.go @@ -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"` +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..7c789ff --- /dev/null +++ b/config/config.go @@ -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 +} diff --git a/config/gin.go b/config/gin.go new file mode 100644 index 0000000..eac5788 --- /dev/null +++ b/config/gin.go @@ -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"` +} diff --git a/config/gorm.go b/config/gorm.go new file mode 100644 index 0000000..1d01de2 --- /dev/null +++ b/config/gorm.go @@ -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"` +} diff --git a/config/jwt.go b/config/jwt.go new file mode 100644 index 0000000..1f6f0d4 --- /dev/null +++ b/config/jwt.go @@ -0,0 +1,8 @@ +package config + +import "time" + +type Jwt struct { + PrivateKey string `mapstructure:"private-key"` + ExpireDuration time.Duration `mapstructure:"expire-duration"` +} diff --git a/config/logger.go b/config/logger.go new file mode 100644 index 0000000..8c51c8b --- /dev/null +++ b/config/logger.go @@ -0,0 +1,7 @@ +package config + +type Logger struct { + Path string + Level string + ReportCaller bool `mapstructure:"report-caller"` +} diff --git a/config/oss.go b/config/oss.go new file mode 100644 index 0000000..6f92f69 --- /dev/null +++ b/config/oss.go @@ -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"` +} diff --git a/config/redis.go b/config/redis.go new file mode 100644 index 0000000..9ff1f2c --- /dev/null +++ b/config/redis.go @@ -0,0 +1,7 @@ +package config + +type Redis struct { + Addr string + Password string + Db int +} diff --git a/config/rustdesk.go b/config/rustdesk.go new file mode 100644 index 0000000..d3ffdf5 --- /dev/null +++ b/config/rustdesk.go @@ -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"` +} diff --git a/docs/admin/admin_docs.go b/docs/admin/admin_docs.go new file mode 100644 index 0000000..80db6ec --- /dev/null +++ b/docs/admin/admin_docs.go @@ -0,0 +1,2331 @@ +// Package admin Code generated by swaggo/swag. DO NOT EDIT +package admin + +import "github.com/swaggo/swag" + +const docTemplateadmin = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/admin/address_book/create": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "创建地址簿", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "地址簿" + ], + "summary": "创建地址簿", + "parameters": [ + { + "description": "地址簿信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.AddressBookForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.AddressBook" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/address_book/delete": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "地址簿删除", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "地址簿" + ], + "summary": "地址簿删除", + "parameters": [ + { + "description": "地址簿信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.AddressBookForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/address_book/detail/{id}": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "地址簿详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "地址簿" + ], + "summary": "地址簿详情", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.AddressBook" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/address_book/list": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "地址簿列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "地址簿" + ], + "summary": "地址簿列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "页大小", + "name": "page_size", + "in": "query" + }, + { + "type": "integer", + "description": "用户id", + "name": "user_id", + "in": "query" + }, + { + "type": "integer", + "description": "是否是我的", + "name": "is_my", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.AddressBookList" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/address_book/update": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "地址簿编辑", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "地址簿" + ], + "summary": "地址簿编辑", + "parameters": [ + { + "description": "地址簿信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.AddressBookForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.AddressBook" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/file/oss_token": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "获取ossToken", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "文件" + ], + "summary": "获取ossToken", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/file/upload": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "上传文件到本地", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "文件" + ], + "summary": "上传文件到本地", + "parameters": [ + { + "type": "file", + "description": "上传文件示例", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/group/create": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "创建群组", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "群组" + ], + "summary": "创建群组", + "parameters": [ + { + "description": "群组信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.GroupForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Group" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/group/delete": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "群组删除", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "群组" + ], + "summary": "群组删除", + "parameters": [ + { + "description": "群组信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.GroupForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/group/detail/{id}": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "群组详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "群组" + ], + "summary": "群组详情", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Group" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/group/list": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "群组列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "群组" + ], + "summary": "群组列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "页大小", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.GroupList" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/group/update": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "群组编辑", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "群组" + ], + "summary": "群组编辑", + "parameters": [ + { + "description": "群组信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.GroupForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Group" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/login": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "登录", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "登录" + ], + "summary": "登录", + "parameters": [ + { + "description": "登录信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Gwen_http_request_admin.Login" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/admin.LoginPayload" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/logout": { + "post": { + "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" + } + } + } + } + }, + "/admin/peer/create": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "创建机器", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "机器" + ], + "summary": "创建机器", + "parameters": [ + { + "description": "机器信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.PeerForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Peer" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/peer/delete": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "机器删除", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "机器" + ], + "summary": "机器删除", + "parameters": [ + { + "description": "机器信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.PeerForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/peer/detail/{id}": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "机器详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "机器" + ], + "summary": "机器详情", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Peer" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/peer/list": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "机器列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "机器" + ], + "summary": "机器列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "页大小", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.PeerList" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/peer/update": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "机器编辑", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "机器" + ], + "summary": "机器编辑", + "parameters": [ + { + "description": "机器信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.PeerForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Peer" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/server-config": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "服务配置,给webclient提供api-server", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ADMIN" + ], + "summary": "服务配置", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/tag/create": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "创建标签", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "标签" + ], + "summary": "创建标签", + "parameters": [ + { + "description": "标签信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.TagForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Tag" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/tag/delete": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "标签删除", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "标签" + ], + "summary": "标签删除", + "parameters": [ + { + "description": "标签信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.TagForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/tag/detail/{id}": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "标签详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "标签" + ], + "summary": "标签详情", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Tag" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/tag/list": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "标签列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "标签" + ], + "summary": "标签列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "页大小", + "name": "page_size", + "in": "query" + }, + { + "type": "integer", + "description": "是否是我的", + "name": "is_my", + "in": "query" + }, + { + "type": "integer", + "description": "用户id", + "name": "user_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.TagList" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/tag/update": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "标签编辑", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "标签" + ], + "summary": "标签编辑", + "parameters": [ + { + "description": "标签信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.TagForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Tag" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/user/changeCurPwd": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "修改当前用户密码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户" + ], + "summary": "修改当前用户密码", + "parameters": [ + { + "description": "用户信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.ChangeCurPasswordForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/user/create": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "创建管理员", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户" + ], + "summary": "创建管理员", + "parameters": [ + { + "description": "管理员信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.UserForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.User" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/user/current": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "当前用户", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户" + ], + "summary": "当前用户", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/admin.LoginPayload" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/user/delete": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "管理员编删除", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户" + ], + "summary": "管理员删除", + "parameters": [ + { + "description": "用户信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.UserForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/user/detail/{id}": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "管理员详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户" + ], + "summary": "管理员详情", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.User" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/user/list": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "管理员列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户" + ], + "summary": "管理员列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "页大小", + "name": "page_size", + "in": "query" + }, + { + "type": "integer", + "description": "账户", + "name": "username", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.UserList" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/user/update": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "管理员编辑", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户" + ], + "summary": "管理员编辑", + "parameters": [ + { + "description": "用户信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.UserForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.User" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/user/updatePassword": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "修改密码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户" + ], + "summary": "修改密码", + "parameters": [ + { + "description": "用户信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.UserPasswordForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + } + }, + "definitions": { + "Gwen_http_request_admin.Login": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "admin.AddressBookForm": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "alias": { + "type": "string" + }, + "force_always_relay": { + "type": "boolean" + }, + "hash": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "id": { + "type": "string" + }, + "login_name": { + "type": "string" + }, + "online": { + "type": "boolean" + }, + "password": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "rdp_port": { + "type": "string" + }, + "rdp_username": { + "type": "string" + }, + "row_id": { + "type": "integer" + }, + "same_server": { + "type": "boolean" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "user_id": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + }, + "admin.ChangeCurPasswordForm": { + "type": "object", + "required": [ + "new_password", + "old_password" + ], + "properties": { + "new_password": { + "type": "string", + "maxLength": 20, + "minLength": 4 + }, + "old_password": { + "type": "string", + "maxLength": 20, + "minLength": 4 + } + } + }, + "admin.GroupForm": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "admin.LoginPayload": { + "type": "object", + "properties": { + "nickname": { + "type": "string" + }, + "route_names": { + "type": "array", + "items": { + "type": "string" + } + }, + "token": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "admin.PeerForm": { + "type": "object", + "properties": { + "cpu": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "id": { + "type": "string" + }, + "memory": { + "type": "string" + }, + "os": { + "type": "string" + }, + "row_id": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "uuid": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "admin.TagForm": { + "type": "object", + "required": [ + "color", + "name", + "user_id" + ], + "properties": { + "color": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, + "admin.UserForm": { + "type": "object", + "required": [ + "group_id", + "nickname", + "status", + "username" + ], + "properties": { + "avatar": { + "type": "string" + }, + "group_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "is_admin": { + "type": "boolean" + }, + "nickname": { + "description": "Password string ` + "`" + `json:\"password\" validate:\"required,gte=4,lte=20\"` + "`" + `", + "type": "string" + }, + "status": { + "minimum": 0, + "allOf": [ + { + "$ref": "#/definitions/model.StatusCode" + } + ] + }, + "username": { + "type": "string", + "maxLength": 10, + "minLength": 4 + } + } + }, + "admin.UserPasswordForm": { + "type": "object", + "required": [ + "id", + "password" + ], + "properties": { + "id": { + "type": "integer" + }, + "password": { + "type": "string", + "maxLength": 20, + "minLength": 4 + } + } + }, + "model.AddressBook": { + "type": "object", + "properties": { + "alias": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "forceAlwaysRelay": { + "type": "boolean" + }, + "hash": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "id": { + "type": "string" + }, + "loginName": { + "type": "string" + }, + "online": { + "type": "boolean" + }, + "password": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "rdpPort": { + "type": "string" + }, + "rdpUsername": { + "type": "string" + }, + "row_id": { + "type": "integer" + }, + "sameServer": { + "type": "boolean" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + }, + "model.AddressBookList": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/model.AddressBook" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "model.Group": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "updated_at": { + "type": "string" + } + } + }, + "model.GroupList": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Group" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "model.Peer": { + "type": "object", + "properties": { + "cpu": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "id": { + "type": "string" + }, + "memory": { + "type": "string" + }, + "os": { + "type": "string" + }, + "row_id": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "user": { + "$ref": "#/definitions/model.User" + }, + "user_id": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "uuid": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "model.PeerList": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Peer" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "model.StatusCode": { + "type": "integer", + "enum": [ + 1, + 2 + ], + "x-enum-comments": { + "COMMON_STATUS_DISABLED": "通用状态 禁用", + "COMMON_STATUS_ENABLE": "通用状态 启用" + }, + "x-enum-varnames": [ + "COMMON_STATUS_ENABLE", + "COMMON_STATUS_DISABLED" + ] + }, + "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" + } + } + }, + "model.TagList": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Tag" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "model.User": { + "type": "object", + "properties": { + "avatar": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "group_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "is_admin": { + "type": "boolean" + }, + "nickname": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/model.StatusCode" + }, + "updated_at": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "model.UserList": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/model.User" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "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" + } + } +}` + +// SwaggerInfoadmin holds exported Swagger Info so clients can modify it +var SwaggerInfoadmin = &swag.Spec{ + Version: "1.0", + Host: "", + BasePath: "/api", + Schemes: []string{}, + Title: "管理系统API", + Description: "接口", + InfoInstanceName: "admin", + SwaggerTemplate: docTemplateadmin, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfoadmin.InstanceName(), SwaggerInfoadmin) +} diff --git a/docs/admin/admin_swagger.json b/docs/admin/admin_swagger.json new file mode 100644 index 0000000..c711955 --- /dev/null +++ b/docs/admin/admin_swagger.json @@ -0,0 +1,2306 @@ +{ + "swagger": "2.0", + "info": { + "description": "接口", + "title": "管理系统API", + "contact": {}, + "version": "1.0" + }, + "basePath": "/api", + "paths": { + "/admin/address_book/create": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "创建地址簿", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "地址簿" + ], + "summary": "创建地址簿", + "parameters": [ + { + "description": "地址簿信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.AddressBookForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.AddressBook" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/address_book/delete": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "地址簿删除", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "地址簿" + ], + "summary": "地址簿删除", + "parameters": [ + { + "description": "地址簿信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.AddressBookForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/address_book/detail/{id}": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "地址簿详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "地址簿" + ], + "summary": "地址簿详情", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.AddressBook" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/address_book/list": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "地址簿列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "地址簿" + ], + "summary": "地址簿列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "页大小", + "name": "page_size", + "in": "query" + }, + { + "type": "integer", + "description": "用户id", + "name": "user_id", + "in": "query" + }, + { + "type": "integer", + "description": "是否是我的", + "name": "is_my", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.AddressBookList" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/address_book/update": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "地址簿编辑", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "地址簿" + ], + "summary": "地址簿编辑", + "parameters": [ + { + "description": "地址簿信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.AddressBookForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.AddressBook" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/file/oss_token": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "获取ossToken", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "文件" + ], + "summary": "获取ossToken", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/file/upload": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "上传文件到本地", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "文件" + ], + "summary": "上传文件到本地", + "parameters": [ + { + "type": "file", + "description": "上传文件示例", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/group/create": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "创建群组", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "群组" + ], + "summary": "创建群组", + "parameters": [ + { + "description": "群组信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.GroupForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Group" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/group/delete": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "群组删除", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "群组" + ], + "summary": "群组删除", + "parameters": [ + { + "description": "群组信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.GroupForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/group/detail/{id}": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "群组详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "群组" + ], + "summary": "群组详情", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Group" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/group/list": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "群组列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "群组" + ], + "summary": "群组列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "页大小", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.GroupList" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/group/update": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "群组编辑", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "群组" + ], + "summary": "群组编辑", + "parameters": [ + { + "description": "群组信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.GroupForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Group" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/login": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "登录", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "登录" + ], + "summary": "登录", + "parameters": [ + { + "description": "登录信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Gwen_http_request_admin.Login" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/admin.LoginPayload" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/logout": { + "post": { + "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" + } + } + } + } + }, + "/admin/peer/create": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "创建机器", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "机器" + ], + "summary": "创建机器", + "parameters": [ + { + "description": "机器信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.PeerForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Peer" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/peer/delete": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "机器删除", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "机器" + ], + "summary": "机器删除", + "parameters": [ + { + "description": "机器信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.PeerForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/peer/detail/{id}": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "机器详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "机器" + ], + "summary": "机器详情", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Peer" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/peer/list": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "机器列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "机器" + ], + "summary": "机器列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "页大小", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.PeerList" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/peer/update": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "机器编辑", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "机器" + ], + "summary": "机器编辑", + "parameters": [ + { + "description": "机器信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.PeerForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Peer" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/server-config": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "服务配置,给webclient提供api-server", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ADMIN" + ], + "summary": "服务配置", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/tag/create": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "创建标签", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "标签" + ], + "summary": "创建标签", + "parameters": [ + { + "description": "标签信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.TagForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Tag" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/tag/delete": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "标签删除", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "标签" + ], + "summary": "标签删除", + "parameters": [ + { + "description": "标签信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.TagForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/tag/detail/{id}": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "标签详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "标签" + ], + "summary": "标签详情", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Tag" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/tag/list": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "标签列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "标签" + ], + "summary": "标签列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "页大小", + "name": "page_size", + "in": "query" + }, + { + "type": "integer", + "description": "是否是我的", + "name": "is_my", + "in": "query" + }, + { + "type": "integer", + "description": "用户id", + "name": "user_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.TagList" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/tag/update": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "标签编辑", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "标签" + ], + "summary": "标签编辑", + "parameters": [ + { + "description": "标签信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.TagForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Tag" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/user/changeCurPwd": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "修改当前用户密码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户" + ], + "summary": "修改当前用户密码", + "parameters": [ + { + "description": "用户信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.ChangeCurPasswordForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/user/create": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "创建管理员", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户" + ], + "summary": "创建管理员", + "parameters": [ + { + "description": "管理员信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.UserForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.User" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/user/current": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "当前用户", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户" + ], + "summary": "当前用户", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/admin.LoginPayload" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/user/delete": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "管理员编删除", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户" + ], + "summary": "管理员删除", + "parameters": [ + { + "description": "用户信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.UserForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/user/detail/{id}": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "管理员详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户" + ], + "summary": "管理员详情", + "parameters": [ + { + "type": "integer", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.User" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/user/list": { + "get": { + "security": [ + { + "token": [] + } + ], + "description": "管理员列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户" + ], + "summary": "管理员列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "页大小", + "name": "page_size", + "in": "query" + }, + { + "type": "integer", + "description": "账户", + "name": "username", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.UserList" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/user/update": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "管理员编辑", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户" + ], + "summary": "管理员编辑", + "parameters": [ + { + "description": "用户信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.UserForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.User" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/user/updatePassword": { + "post": { + "security": [ + { + "token": [] + } + ], + "description": "修改密码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户" + ], + "summary": "修改密码", + "parameters": [ + { + "description": "用户信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.UserPasswordForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + } + }, + "definitions": { + "Gwen_http_request_admin.Login": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "admin.AddressBookForm": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "alias": { + "type": "string" + }, + "force_always_relay": { + "type": "boolean" + }, + "hash": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "id": { + "type": "string" + }, + "login_name": { + "type": "string" + }, + "online": { + "type": "boolean" + }, + "password": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "rdp_port": { + "type": "string" + }, + "rdp_username": { + "type": "string" + }, + "row_id": { + "type": "integer" + }, + "same_server": { + "type": "boolean" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "user_id": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + }, + "admin.ChangeCurPasswordForm": { + "type": "object", + "required": [ + "new_password", + "old_password" + ], + "properties": { + "new_password": { + "type": "string", + "maxLength": 20, + "minLength": 4 + }, + "old_password": { + "type": "string", + "maxLength": 20, + "minLength": 4 + } + } + }, + "admin.GroupForm": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "admin.LoginPayload": { + "type": "object", + "properties": { + "nickname": { + "type": "string" + }, + "route_names": { + "type": "array", + "items": { + "type": "string" + } + }, + "token": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "admin.PeerForm": { + "type": "object", + "properties": { + "cpu": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "id": { + "type": "string" + }, + "memory": { + "type": "string" + }, + "os": { + "type": "string" + }, + "row_id": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "uuid": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "admin.TagForm": { + "type": "object", + "required": [ + "color", + "name", + "user_id" + ], + "properties": { + "color": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, + "admin.UserForm": { + "type": "object", + "required": [ + "group_id", + "nickname", + "status", + "username" + ], + "properties": { + "avatar": { + "type": "string" + }, + "group_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "is_admin": { + "type": "boolean" + }, + "nickname": { + "description": "Password string `json:\"password\" validate:\"required,gte=4,lte=20\"`", + "type": "string" + }, + "status": { + "minimum": 0, + "allOf": [ + { + "$ref": "#/definitions/model.StatusCode" + } + ] + }, + "username": { + "type": "string", + "maxLength": 10, + "minLength": 4 + } + } + }, + "admin.UserPasswordForm": { + "type": "object", + "required": [ + "id", + "password" + ], + "properties": { + "id": { + "type": "integer" + }, + "password": { + "type": "string", + "maxLength": 20, + "minLength": 4 + } + } + }, + "model.AddressBook": { + "type": "object", + "properties": { + "alias": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "forceAlwaysRelay": { + "type": "boolean" + }, + "hash": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "id": { + "type": "string" + }, + "loginName": { + "type": "string" + }, + "online": { + "type": "boolean" + }, + "password": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "rdpPort": { + "type": "string" + }, + "rdpUsername": { + "type": "string" + }, + "row_id": { + "type": "integer" + }, + "sameServer": { + "type": "boolean" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + }, + "model.AddressBookList": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/model.AddressBook" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "model.Group": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "updated_at": { + "type": "string" + } + } + }, + "model.GroupList": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Group" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "model.Peer": { + "type": "object", + "properties": { + "cpu": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "id": { + "type": "string" + }, + "memory": { + "type": "string" + }, + "os": { + "type": "string" + }, + "row_id": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "user": { + "$ref": "#/definitions/model.User" + }, + "user_id": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "uuid": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "model.PeerList": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Peer" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "model.StatusCode": { + "type": "integer", + "enum": [ + 1, + 2 + ], + "x-enum-comments": { + "COMMON_STATUS_DISABLED": "通用状态 禁用", + "COMMON_STATUS_ENABLE": "通用状态 启用" + }, + "x-enum-varnames": [ + "COMMON_STATUS_ENABLE", + "COMMON_STATUS_DISABLED" + ] + }, + "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" + } + } + }, + "model.TagList": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Tag" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "model.User": { + "type": "object", + "properties": { + "avatar": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "group_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "is_admin": { + "type": "boolean" + }, + "nickname": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/model.StatusCode" + }, + "updated_at": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "model.UserList": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/model.User" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "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" + } + } +} \ No newline at end of file diff --git a/docs/admin/admin_swagger.yaml b/docs/admin/admin_swagger.yaml new file mode 100644 index 0000000..0a0584b --- /dev/null +++ b/docs/admin/admin_swagger.yaml @@ -0,0 +1,1411 @@ +basePath: /api +definitions: + Gwen_http_request_admin.Login: + properties: + password: + type: string + username: + type: string + required: + - password + - username + type: object + admin.AddressBookForm: + properties: + alias: + type: string + force_always_relay: + type: boolean + hash: + type: string + hostname: + type: string + id: + type: string + login_name: + type: string + online: + type: boolean + password: + type: string + platform: + type: string + rdp_port: + type: string + rdp_username: + type: string + row_id: + type: integer + same_server: + type: boolean + tags: + items: + type: string + type: array + user_id: + type: integer + username: + type: string + required: + - id + type: object + admin.ChangeCurPasswordForm: + properties: + new_password: + maxLength: 20 + minLength: 4 + type: string + old_password: + maxLength: 20 + minLength: 4 + type: string + required: + - new_password + - old_password + type: object + admin.GroupForm: + properties: + id: + type: integer + name: + type: string + required: + - name + type: object + admin.LoginPayload: + properties: + nickname: + type: string + route_names: + items: + type: string + type: array + token: + type: string + username: + type: string + type: object + admin.PeerForm: + properties: + cpu: + type: string + hostname: + type: string + id: + type: string + memory: + type: string + os: + type: string + row_id: + type: integer + username: + type: string + uuid: + type: string + version: + type: string + type: object + admin.TagForm: + properties: + color: + type: integer + id: + type: integer + name: + type: string + user_id: + type: integer + required: + - color + - name + - user_id + type: object + admin.UserForm: + properties: + avatar: + type: string + group_id: + type: integer + id: + type: integer + is_admin: + type: boolean + nickname: + description: Password string `json:"password" validate:"required,gte=4,lte=20"` + type: string + status: + allOf: + - $ref: '#/definitions/model.StatusCode' + minimum: 0 + username: + maxLength: 10 + minLength: 4 + type: string + required: + - group_id + - nickname + - status + - username + type: object + admin.UserPasswordForm: + properties: + id: + type: integer + password: + maxLength: 20 + minLength: 4 + type: string + required: + - id + - password + type: object + model.AddressBook: + properties: + alias: + type: string + created_at: + type: string + forceAlwaysRelay: + type: boolean + hash: + type: string + hostname: + type: string + id: + type: string + loginName: + type: string + online: + type: boolean + password: + type: string + platform: + type: string + rdpPort: + type: string + rdpUsername: + type: string + row_id: + type: integer + sameServer: + type: boolean + tags: + items: + type: string + type: array + updated_at: + type: string + user_id: + type: integer + username: + type: string + type: object + model.AddressBookList: + properties: + list: + items: + $ref: '#/definitions/model.AddressBook' + type: array + page: + type: integer + page_size: + type: integer + total: + type: integer + type: object + model.Group: + properties: + created_at: + type: string + id: + type: integer + name: + type: string + type: + type: integer + updated_at: + type: string + type: object + model.GroupList: + properties: + list: + items: + $ref: '#/definitions/model.Group' + type: array + page: + type: integer + page_size: + type: integer + total: + type: integer + type: object + model.Peer: + properties: + cpu: + type: string + created_at: + type: string + hostname: + type: string + id: + type: string + memory: + type: string + os: + type: string + row_id: + type: integer + updated_at: + type: string + user: + $ref: '#/definitions/model.User' + user_id: + type: integer + username: + type: string + uuid: + type: string + version: + type: string + type: object + model.PeerList: + properties: + list: + items: + $ref: '#/definitions/model.Peer' + type: array + page: + type: integer + page_size: + type: integer + total: + type: integer + type: object + model.StatusCode: + enum: + - 1 + - 2 + type: integer + x-enum-comments: + COMMON_STATUS_DISABLED: 通用状态 禁用 + COMMON_STATUS_ENABLE: 通用状态 启用 + x-enum-varnames: + - COMMON_STATUS_ENABLE + - COMMON_STATUS_DISABLED + 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 + model.TagList: + properties: + list: + items: + $ref: '#/definitions/model.Tag' + type: array + page: + type: integer + page_size: + type: integer + total: + type: integer + type: object + model.User: + properties: + avatar: + type: string + created_at: + type: string + group_id: + type: integer + id: + type: integer + is_admin: + type: boolean + nickname: + type: string + status: + $ref: '#/definitions/model.StatusCode' + updated_at: + type: string + username: + type: string + type: object + model.UserList: + properties: + list: + items: + $ref: '#/definitions/model.User' + type: array + page: + type: integer + page_size: + type: integer + total: + type: integer + type: object + response.Response: + properties: + code: + type: integer + data: {} + message: + type: string + type: object +info: + contact: {} + description: 接口 + title: 管理系统API + version: "1.0" +paths: + /admin/address_book/create: + post: + consumes: + - application/json + description: 创建地址簿 + parameters: + - description: 地址簿信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/admin.AddressBookForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.AddressBook' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 创建地址簿 + tags: + - 地址簿 + /admin/address_book/delete: + post: + consumes: + - application/json + description: 地址簿删除 + parameters: + - description: 地址簿信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/admin.AddressBookForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 地址簿删除 + tags: + - 地址簿 + /admin/address_book/detail/{id}: + get: + consumes: + - application/json + description: 地址簿详情 + parameters: + - description: ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.AddressBook' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 地址簿详情 + tags: + - 地址簿 + /admin/address_book/list: + get: + consumes: + - application/json + description: 地址簿列表 + parameters: + - description: 页码 + in: query + name: page + type: integer + - description: 页大小 + in: query + name: page_size + type: integer + - description: 用户id + in: query + name: user_id + type: integer + - description: 是否是我的 + in: query + name: is_my + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.AddressBookList' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 地址簿列表 + tags: + - 地址簿 + /admin/address_book/update: + post: + consumes: + - application/json + description: 地址簿编辑 + parameters: + - description: 地址簿信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/admin.AddressBookForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.AddressBook' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 地址簿编辑 + tags: + - 地址簿 + /admin/file/oss_token: + get: + consumes: + - application/json + description: 获取ossToken + 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: 获取ossToken + tags: + - 文件 + /admin/file/upload: + post: + consumes: + - multipart/form-data + description: 上传文件到本地 + parameters: + - description: 上传文件示例 + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 上传文件到本地 + tags: + - 文件 + /admin/group/create: + post: + consumes: + - application/json + description: 创建群组 + parameters: + - description: 群组信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/admin.GroupForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.Group' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 创建群组 + tags: + - 群组 + /admin/group/delete: + post: + consumes: + - application/json + description: 群组删除 + parameters: + - description: 群组信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/admin.GroupForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 群组删除 + tags: + - 群组 + /admin/group/detail/{id}: + get: + consumes: + - application/json + description: 群组详情 + parameters: + - description: ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.Group' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 群组详情 + tags: + - 群组 + /admin/group/list: + get: + consumes: + - application/json + description: 群组列表 + parameters: + - description: 页码 + in: query + name: page + type: integer + - description: 页大小 + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.GroupList' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 群组列表 + tags: + - 群组 + /admin/group/update: + post: + consumes: + - application/json + description: 群组编辑 + parameters: + - description: 群组信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/admin.GroupForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.Group' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 群组编辑 + tags: + - 群组 + /admin/login: + post: + consumes: + - application/json + description: 登录 + parameters: + - description: 登录信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/Gwen_http_request_admin.Login' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/admin.LoginPayload' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 登录 + tags: + - 登录 + /admin/logout: + post: + 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: + - 登录 + /admin/peer/create: + post: + consumes: + - application/json + description: 创建机器 + parameters: + - description: 机器信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/admin.PeerForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.Peer' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 创建机器 + tags: + - 机器 + /admin/peer/delete: + post: + consumes: + - application/json + description: 机器删除 + parameters: + - description: 机器信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/admin.PeerForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 机器删除 + tags: + - 机器 + /admin/peer/detail/{id}: + get: + consumes: + - application/json + description: 机器详情 + parameters: + - description: ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.Peer' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 机器详情 + tags: + - 机器 + /admin/peer/list: + get: + consumes: + - application/json + description: 机器列表 + parameters: + - description: 页码 + in: query + name: page + type: integer + - description: 页大小 + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.PeerList' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 机器列表 + tags: + - 机器 + /admin/peer/update: + post: + consumes: + - application/json + description: 机器编辑 + parameters: + - description: 机器信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/admin.PeerForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.Peer' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 机器编辑 + tags: + - 机器 + /admin/server-config: + get: + consumes: + - application/json + description: 服务配置,给webclient提供api-server + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 服务配置 + tags: + - ADMIN + /admin/tag/create: + post: + consumes: + - application/json + description: 创建标签 + parameters: + - description: 标签信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/admin.TagForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.Tag' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 创建标签 + tags: + - 标签 + /admin/tag/delete: + post: + consumes: + - application/json + description: 标签删除 + parameters: + - description: 标签信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/admin.TagForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 标签删除 + tags: + - 标签 + /admin/tag/detail/{id}: + get: + consumes: + - application/json + description: 标签详情 + parameters: + - description: ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.Tag' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 标签详情 + tags: + - 标签 + /admin/tag/list: + get: + consumes: + - application/json + description: 标签列表 + parameters: + - description: 页码 + in: query + name: page + type: integer + - description: 页大小 + in: query + name: page_size + type: integer + - description: 是否是我的 + in: query + name: is_my + type: integer + - description: 用户id + in: query + name: user_id + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.TagList' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 标签列表 + tags: + - 标签 + /admin/tag/update: + post: + consumes: + - application/json + description: 标签编辑 + parameters: + - description: 标签信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/admin.TagForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.Tag' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 标签编辑 + tags: + - 标签 + /admin/user/changeCurPwd: + post: + consumes: + - application/json + description: 修改当前用户密码 + parameters: + - description: 用户信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/admin.ChangeCurPasswordForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 修改当前用户密码 + tags: + - 用户 + /admin/user/create: + post: + consumes: + - application/json + description: 创建管理员 + parameters: + - description: 管理员信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/admin.UserForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.User' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 创建管理员 + tags: + - 用户 + /admin/user/current: + get: + consumes: + - application/json + description: 当前用户 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/admin.LoginPayload' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 当前用户 + tags: + - 用户 + /admin/user/delete: + post: + consumes: + - application/json + description: 管理员编删除 + parameters: + - description: 用户信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/admin.UserForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 管理员删除 + tags: + - 用户 + /admin/user/detail/{id}: + get: + consumes: + - application/json + description: 管理员详情 + parameters: + - description: ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.User' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 管理员详情 + tags: + - 用户 + /admin/user/list: + get: + consumes: + - application/json + description: 管理员列表 + parameters: + - description: 页码 + in: query + name: page + type: integer + - description: 页大小 + in: query + name: page_size + type: integer + - description: 账户 + in: query + name: username + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.UserList' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 管理员列表 + tags: + - 用户 + /admin/user/update: + post: + consumes: + - application/json + description: 管理员编辑 + parameters: + - description: 用户信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/admin.UserForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.User' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.Response' + security: + - token: [] + summary: 管理员编辑 + tags: + - 用户 + /admin/user/updatePassword: + post: + consumes: + - application/json + description: 修改密码 + parameters: + - description: 用户信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/admin.UserPasswordForm' + 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: + - 用户 +securityDefinitions: + BearerAuth: + in: header + name: Authorization + type: apiKey + token: + in: header + name: api-token + type: apiKey +swagger: "2.0" diff --git a/docs/api/api_docs.go b/docs/api/api_docs.go new file mode 100644 index 0000000..57ecdef --- /dev/null +++ b/docs/api/api_docs.go @@ -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) +} diff --git a/docs/api/api_swagger.json b/docs/api/api_swagger.json new file mode 100644 index 0000000..ffed862 --- /dev/null +++ b/docs/api/api_swagger.json @@ -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" + } + } +} \ No newline at end of file diff --git a/docs/api/api_swagger.yaml b/docs/api/api_swagger.yaml new file mode 100644 index 0000000..e5f718f --- /dev/null +++ b/docs/api/api_swagger.yaml @@ -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" diff --git a/docs/api_swag.png b/docs/api_swag.png new file mode 100644 index 0000000000000000000000000000000000000000..9d7fe7496e4666057d41fbc3081516595a6a0b88 GIT binary patch literal 56623 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

97D3;_9nI5lfs0Wn*Mt^ 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 literal 0 HcmV?d00001 diff --git a/docs/pc_ab.png b/docs/pc_ab.png new file mode 100644 index 0000000000000000000000000000000000000000..fb8928a3e34470342c7708234dee07bd7a973681 GIT binary patch literal 54795 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

^^T)8?MWse*#D)#cbn-}_eXnq;#jI{S=< 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 DqVFxE literal 0 HcmV?d00001 diff --git a/docs/web_resetpwd.png b/docs/web_resetpwd.png new file mode 100644 index 0000000000000000000000000000000000000000..3f5c679ed2e852c1693806abacdf540911f2f6a3 GIT binary patch literal 3710 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 literal 0 HcmV?d00001 diff --git a/docs/web_user.png b/docs/web_user.png new file mode 100644 index 0000000000000000000000000000000000000000..dd405d8e15cb5d876d95723f4f32c75ce0f491c9 GIT binary patch literal 27555 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

a;dQC* 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

  • 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 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, "信息不存在") +} diff --git a/http/controller/admin/file.go b/http/controller/admin/file.go new file mode 100644 index 0000000..62564e8 --- /dev/null +++ b/http/controller/admin/file.go @@ -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, + }) +} diff --git a/http/controller/admin/group.go b/http/controller/admin/group.go new file mode 100644 index 0000000..ae87edf --- /dev/null +++ b/http/controller/admin/group.go @@ -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, "信息不存在") +} diff --git a/http/controller/admin/login.go b/http/controller/admin/login.go new file mode 100644 index 0000000..dba0c6d --- /dev/null +++ b/http/controller/admin/login.go @@ -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) +} diff --git a/http/controller/admin/peer.go b/http/controller/admin/peer.go new file mode 100644 index 0000000..5ab048d --- /dev/null +++ b/http/controller/admin/peer.go @@ -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, "信息不存在") +} diff --git a/http/controller/admin/rustdesk.go b/http/controller/admin/rustdesk.go new file mode 100644 index 0000000..382190b --- /dev/null +++ b/http/controller/admin/rustdesk.go @@ -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) +} diff --git a/http/controller/admin/tag.go b/http/controller/admin/tag.go new file mode 100644 index 0000000..305ef0b --- /dev/null +++ b/http/controller/admin/tag.go @@ -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, "信息不存在") +} diff --git a/http/controller/admin/user.go b/http/controller/admin/user.go new file mode 100644 index 0000000..45bcce6 --- /dev/null +++ b/http/controller/admin/user.go @@ -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) +} diff --git a/http/controller/api/ab.go b/http/controller/api/ab.go new file mode 100644 index 0000000..ddfb026 --- /dev/null +++ b/http/controller/api/ab.go @@ -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, "") +} diff --git a/http/controller/api/group.go b/http/controller/api/group.go new file mode 100644 index 0000000..31c3c0f --- /dev/null +++ b/http/controller/api/group.go @@ -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, + }) +} diff --git a/http/controller/api/index.go b/http/controller/api/index.go new file mode 100644 index 0000000..e58f2bc --- /dev/null +++ b/http/controller/api/index.go @@ -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{}) +} diff --git a/http/controller/api/login.go b/http/controller/api/login.go new file mode 100644 index 0000000..5fd1c65 --- /dev/null +++ b/http/controller/api/login.go @@ -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) + +} diff --git a/http/controller/api/peer.go b/http/controller/api/peer.go new file mode 100644 index 0000000..985ddd8 --- /dev/null +++ b/http/controller/api/peer.go @@ -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, "") +} diff --git a/http/controller/api/user.go b/http/controller/api/user.go new file mode 100644 index 0000000..a8ccc20 --- /dev/null +++ b/http/controller/api/user.go @@ -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, + }) +} diff --git a/http/controller/api/webClient.go b/http/controller/api/webClient.go new file mode 100644 index 0000000..34c73c0 --- /dev/null +++ b/http/controller/api/webClient.go @@ -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, + }, + ) +} diff --git a/http/controller/web/index.go b/http/controller/web/index.go new file mode 100644 index 0000000..7e549ec --- /dev/null +++ b/http/controller/web/index.go @@ -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) +} diff --git a/http/http.go b/http/http.go new file mode 100644 index 0000000..3908339 --- /dev/null +++ b/http/http.go @@ -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) +} diff --git a/http/middleware/admin.go b/http/middleware/admin.go new file mode 100644 index 0000000..f312f9b --- /dev/null +++ b/http/middleware/admin.go @@ -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() + } +} diff --git a/http/middleware/admin_privilege.go b/http/middleware/admin_privilege.go new file mode 100644 index 0000000..bd1267d --- /dev/null +++ b/http/middleware/admin_privilege.go @@ -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() + } +} diff --git a/http/middleware/cors.go b/http/middleware/cors.go new file mode 100644 index 0000000..f60a462 --- /dev/null +++ b/http/middleware/cors.go @@ -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() + } +} diff --git a/http/middleware/jwt.go b/http/middleware/jwt.go new file mode 100644 index 0000000..b9b0599 --- /dev/null +++ b/http/middleware/jwt.go @@ -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() + } +} diff --git a/http/middleware/logger.go b/http/middleware/logger.go new file mode 100644 index 0000000..53af2e9 --- /dev/null +++ b/http/middleware/logger.go @@ -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() + } +} diff --git a/http/middleware/rustauth.go b/http/middleware/rustauth.go new file mode 100644 index 0000000..a627968 --- /dev/null +++ b/http/middleware/rustauth.go @@ -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() + } +} diff --git a/http/request/admin/addressBook.go b/http/request/admin/addressBook.go new file mode 100644 index 0000000..5a25fbc --- /dev/null +++ b/http/request/admin/addressBook.go @@ -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 +} diff --git a/http/request/admin/group.go b/http/request/admin/group.go new file mode 100644 index 0000000..dc2d35d --- /dev/null +++ b/http/request/admin/group.go @@ -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 +} diff --git a/http/request/admin/login.go b/http/request/admin/login.go new file mode 100644 index 0000000..691f7a7 --- /dev/null +++ b/http/request/admin/login.go @@ -0,0 +1,6 @@ +package admin + +type Login struct { + Username string `json:"username" validate:"required" label:"用户名"` + Password string `json:"password,omitempty" validate:"required" label:"密码"` +} diff --git a/http/request/admin/peer.go b/http/request/admin/peer.go new file mode 100644 index 0000000..6b35411 --- /dev/null +++ b/http/request/admin/peer.go @@ -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, + } +} diff --git a/http/request/admin/tag.go b/http/request/admin/tag.go new file mode 100644 index 0000000..3a07377 --- /dev/null +++ b/http/request/admin/tag.go @@ -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 +} diff --git a/http/request/admin/user.go b/http/request/admin/user.go new file mode 100644 index 0000000..6e09796 --- /dev/null +++ b/http/request/admin/user.go @@ -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"` +} diff --git a/http/request/api/peer.go b/http/request/api/peer.go new file mode 100644 index 0000000..00955e4 --- /dev/null +++ b/http/request/api/peer.go @@ -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, + } +} diff --git a/http/request/api/user.go b/http/request/api/user.go new file mode 100644 index 0000000..13a0eb4 --- /dev/null +++ b/http/request/api/user.go @@ -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"` +} diff --git a/http/response/admin/user.go b/http/response/admin/user.go new file mode 100644 index 0000000..27e92a9 --- /dev/null +++ b/http/response/admin/user.go @@ -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{"*"} diff --git a/http/response/api/ab.go b/http/response/api/ab.go new file mode 100644 index 0000000..47134e0 --- /dev/null +++ b/http/response/api/ab.go @@ -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"` +} diff --git a/http/response/api/peer.go b/http/response/api/peer.go new file mode 100644 index 0000000..3ad06a4 --- /dev/null +++ b/http/response/api/peer.go @@ -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 info = {}; + int? status; + String user = ''; + String user_name = ''; + String note = ''; + + PeerPayload.fromJson(Map json) + : id = json['id'] ?? '', + info = (json['info'] is Map) ? 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 +// } +//} diff --git a/http/response/api/user.go b/http/response/api/user.go new file mode 100644 index 0000000..dcea9b7 --- /dev/null +++ b/http/response/api/user.go @@ -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"` +} diff --git a/http/response/api/webClient.go b/http/response/api/webClient.go new file mode 100644 index 0000000..9245e91 --- /dev/null +++ b/http/response/api/webClient.go @@ -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, + } +} diff --git a/http/response/response.go b/http/response/response.go new file mode 100644 index 0000000..9f51c26 --- /dev/null +++ b/http/response/response.go @@ -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"` +} diff --git a/http/router/admin.go b/http/router/admin.go new file mode 100644 index 0000000..df93210 --- /dev/null +++ b/http/router/admin.go @@ -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) + } +}*/ diff --git a/http/router/api.go b/http/router/api.go new file mode 100644 index 0000000..aa4695f --- /dev/null +++ b/http/router/api.go @@ -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")) +} diff --git a/http/router/router.go b/http/router/router.go new file mode 100644 index 0000000..2d108cb --- /dev/null +++ b/http/router/router.go @@ -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")) +} diff --git a/http/run.go b/http/run.go new file mode 100644 index 0000000..a4e2e13 --- /dev/null +++ b/http/run.go @@ -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) +} diff --git a/http/run_win.go b/http/run_win.go new file mode 100644 index 0000000..c6339b5 --- /dev/null +++ b/http/run_win.go @@ -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) +} diff --git a/lib/cache/cache.go b/lib/cache/cache.go new file mode 100644 index 0000000..dbe2c56 --- /dev/null +++ b/lib/cache/cache.go @@ -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 +} diff --git a/lib/cache/cache_test.go b/lib/cache/cache_test.go new file mode 100644 index 0000000..34ca2ab --- /dev/null +++ b/lib/cache/cache_test.go @@ -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) +} diff --git a/lib/cache/file.go b/lib/cache/file.go new file mode 100644 index 0000000..5fb57fd --- /dev/null +++ b/lib/cache/file.go @@ -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(), + } +} diff --git a/lib/cache/file_test.go b/lib/cache/file_test.go new file mode 100644 index 0000000..b8446f5 --- /dev/null +++ b/lib/cache/file_test.go @@ -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) + } +} diff --git a/lib/cache/memory.go b/lib/cache/memory.go new file mode 100644 index 0000000..49d8f9d --- /dev/null +++ b/lib/cache/memory.go @@ -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 +} diff --git a/lib/cache/memory_test.go b/lib/cache/memory_test.go new file mode 100644 index 0000000..89243e7 --- /dev/null +++ b/lib/cache/memory_test.go @@ -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) + } +} diff --git a/lib/cache/redis.go b/lib/cache/redis.go new file mode 100644 index 0000000..5ad957a --- /dev/null +++ b/lib/cache/redis.go @@ -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 +} diff --git a/lib/cache/redis_test.go b/lib/cache/redis_test.go new file mode 100644 index 0000000..c8052cd --- /dev/null +++ b/lib/cache/redis_test.go @@ -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) + } +} diff --git a/lib/cache/simple_cache.go b/lib/cache/simple_cache.go new file mode 100644 index 0000000..f599bd7 --- /dev/null +++ b/lib/cache/simple_cache.go @@ -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{}), + } +} diff --git a/lib/cache/simple_cache_test.go b/lib/cache/simple_cache_test.go new file mode 100644 index 0000000..01c3b4d --- /dev/null +++ b/lib/cache/simple_cache_test.go @@ -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("读取错误") + //} +} diff --git a/lib/jwt/jwt.go b/lib/jwt/jwt.go new file mode 100644 index 0000000..0fdc587 --- /dev/null +++ b/lib/jwt/jwt.go @@ -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 +} diff --git a/lib/jwt/jwt_test.go b/lib/jwt/jwt_test.go new file mode 100644 index 0000000..b40d80c --- /dev/null +++ b/lib/jwt/jwt_test.go @@ -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) + } + +} diff --git a/lib/lock/local.go b/lib/lock/local.go new file mode 100644 index 0000000..a6c492b --- /dev/null +++ b/lib/lock/local.go @@ -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{}, + } +} diff --git a/lib/lock/local_test.go b/lib/lock/local_test.go new file mode 100644 index 0000000..e2ee037 --- /dev/null +++ b/lib/lock/local_test.go @@ -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() +} diff --git a/lib/lock/lock.go b/lib/lock/lock.go new file mode 100644 index 0000000..ca27461 --- /dev/null +++ b/lib/lock/lock.go @@ -0,0 +1,9 @@ +package lock + +import "sync" + +type Locker interface { + GetLock(key string) *sync.Mutex + Lock(key string) + UnLock(key string) +} diff --git a/lib/logger/logger.go b/lib/logger/logger.go new file mode 100644 index 0000000..bc6d39c --- /dev/null +++ b/lib/logger/logger.go @@ -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() +} diff --git a/lib/orm/mysql.go b/lib/orm/mysql.go new file mode 100644 index 0000000..8fc9ed8 --- /dev/null +++ b/lib/orm/mysql.go @@ -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 +} diff --git a/lib/orm/sqlite.go b/lib/orm/sqlite.go new file mode 100644 index 0000000..1186da6 --- /dev/null +++ b/lib/orm/sqlite.go @@ -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 +} diff --git a/lib/upload/local.go b/lib/upload/local.go new file mode 100644 index 0000000..a4326bc --- /dev/null +++ b/lib/upload/local.go @@ -0,0 +1,4 @@ +package upload + +type Local struct { +} diff --git a/lib/upload/oss.go b/lib/upload/oss.go new file mode 100644 index 0000000..7bada1d --- /dev/null +++ b/lib/upload/oss.go @@ -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 +} diff --git a/model/addressBook.go b/model/addressBook.go new file mode 100644 index 0000000..6750398 --- /dev/null +++ b/model/addressBook.go @@ -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 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 +} diff --git a/model/custom_types/auto_json.go b/model/custom_types/auto_json.go new file mode 100644 index 0000000..2538f06 --- /dev/null +++ b/model/custom_types/auto_json.go @@ -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) +} diff --git a/model/custom_types/auto_time.go b/model/custom_types/auto_time.go new file mode 100644 index 0000000..9e9e13b --- /dev/null +++ b/model/custom_types/auto_time.go @@ -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 +} diff --git a/model/group.go b/model/group.go new file mode 100644 index 0000000..4d09b54 --- /dev/null +++ b/model/group.go @@ -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 +} diff --git a/model/model.go b/model/model.go new file mode 100644 index 0000000..8cc5615 --- /dev/null +++ b/model/model.go @@ -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"` +} diff --git a/model/peer.go b/model/peer.go new file mode 100644 index 0000000..ad1ed6e --- /dev/null +++ b/model/peer.go @@ -0,0 +1,21 @@ +package model + +type Peer struct { + RowId uint `json:"row_id" gorm:"primaryKey;"` + Id string `json:"id" gorm:"default:'';not null;index"` + Cpu string `json:"cpu" gorm:"default:'';not null;"` + Hostname string `json:"hostname" gorm:"default:'';not null;"` + Memory string `json:"memory" gorm:"default:'';not null;"` + Os string `json:"os" gorm:"default:'';not null;"` + Username string `json:"username" gorm:"default:'';not null;"` + Uuid string `json:"uuid" gorm:"default:'';not null;index"` + Version string `json:"version" gorm:"default:'';not null;"` + UserId uint `json:"user_id" gorm:"default:0;not null;index"` + User User `json:"user,omitempty" gorm:""` + TimeModel +} + +type PeerList struct { + Peers []*Peer `json:"list"` + Pagination +} diff --git a/model/tag.go b/model/tag.go new file mode 100644 index 0000000..ccc9bfc --- /dev/null +++ b/model/tag.go @@ -0,0 +1,14 @@ +package model + +type Tag struct { + IdModel + Name string `json:"name" gorm:"default:'';not null;"` + UserId uint `json:"user_id" gorm:"default:0;not null;index"` + Color uint `json:"color" gorm:"default:0;not null;"` //color 是flutter的颜色值,从0x00000000 到 0xFFFFFFFF; 前两位表示透明度,后面6位表示颜色, 可以转成rgba + TimeModel +} + +type TagList struct { + Tags []*Tag `json:"list"` + Pagination +} diff --git a/model/user.go b/model/user.go new file mode 100644 index 0000000..32af6d7 --- /dev/null +++ b/model/user.go @@ -0,0 +1,18 @@ +package model + +type User struct { + IdModel + Username string `json:"username" gorm:"default:'';not null;index,unique"` + Password string `json:"-" gorm:"default:'';not null;"` + Nickname string `json:"nickname" gorm:"default:'';not null;"` + Avatar string `json:"avatar" gorm:"default:'';not null;"` + GroupId uint `json:"group_id" gorm:"default:0;not null;index"` + IsAdmin *bool `json:"is_admin" gorm:"default:0;not null;"` + Status StatusCode `json:"status" gorm:"default:1;not null;"` + TimeModel +} + +type UserList struct { + Users []*User `json:"list,omitempty"` + Pagination +} diff --git a/model/userToken.go b/model/userToken.go new file mode 100644 index 0000000..73c3339 --- /dev/null +++ b/model/userToken.go @@ -0,0 +1,9 @@ +package model + +type UserToken struct { + IdModel + UserId uint `json:"user_id" gorm:"default:0;not null;index"` + Token string `json:"token" gorm:"default:'';not null;index"` + ExpiredAt int64 `json:"expired_at" gorm:"default:0;not null;"` + TimeModel +} diff --git a/model/version.go b/model/version.go new file mode 100644 index 0000000..5b46e1f --- /dev/null +++ b/model/version.go @@ -0,0 +1,7 @@ +package model + +type Version struct { + IdModel + Version uint `json:"version" gorm:"default:0;not null;"` + TimeModel +} diff --git a/service/addressBook.go b/service/addressBook.go new file mode 100644 index 0000000..f4fdf75 --- /dev/null +++ b/service/addressBook.go @@ -0,0 +1,109 @@ +package service + +import ( + "Gwen/global" + "Gwen/model" + "gorm.io/gorm" +) + +type AddressBookService struct { +} + +func (s *AddressBookService) Info(id uint) *model.AddressBook { + p := &model.AddressBook{} + global.DB.Where("id = ?", id).First(p) + return p +} +func (s *AddressBookService) InfoByRowId(id uint) *model.AddressBook { + p := &model.AddressBook{} + global.DB.Where("row_id = ?", id).First(p) + return p +} +func (s *AddressBookService) ListByUserId(userId, page, pageSize uint) (res *model.AddressBookList) { + res = s.List(page, pageSize, func(tx *gorm.DB) { + tx.Where("user_id = ?", userId) + }) + return +} +func (s *AddressBookService) ListByUserIds(userIds []uint, page, pageSize uint) (res *model.AddressBookList) { + res = s.List(page, pageSize, func(tx *gorm.DB) { + tx.Where("user_id in (?)", userIds) + }) + return +} + +// AddAddressBook +func (s *AddressBookService) AddAddressBook(ab *model.AddressBook) error { + return global.DB.Create(ab).Error +} + +// UpdateAddressBook +func (s *AddressBookService) UpdateAddressBook(abs []*model.AddressBook, userId uint) error { + //比较peers和数据库中的数据,如果peers中的数据在数据库中不存在,则添加,如果存在则更新,如果数据库中的数据在peers中不存在,则删除 + // 开始事务 + tx := global.DB.Begin() + //1. 获取数据库中的数据 + var dbABs []*model.AddressBook + tx.Where("user_id = ?", userId).Find(&dbABs) + //2. 比较peers和数据库中的数据 + //2.1 获取peers中的id + aBIds := make(map[string]*model.AddressBook) + for _, ab := range abs { + aBIds[ab.Id] = ab + } + //2.2 获取数据库中的id + dbABIds := make(map[string]*model.AddressBook) + for _, dbAb := range dbABs { + dbABIds[dbAb.Id] = dbAb + } + //2.3 比较peers和数据库中的数据 + for id, ab := range aBIds { + dbAB, ok := dbABIds[id] + ab.UserId = userId + if !ok { + //添加 + tx.Create(ab) + } else { + //更新 + tx.Model(&model.AddressBook{}).Where("row_id = ?", dbAB.RowId).Updates(ab) + } + } + //2.4 删除 + for id, dbAB := range dbABIds { + _, ok := aBIds[id] + if !ok { + tx.Delete(dbAB) + } + } + tx.Commit() + return nil + +} + +func (t *AddressBookService) List(page, pageSize uint, where func(tx *gorm.DB)) (res *model.AddressBookList) { + res = &model.AddressBookList{} + res.Page = int64(page) + res.PageSize = int64(pageSize) + tx := global.DB.Model(&model.AddressBook{}) + if where != nil { + where(tx) + } + tx.Count(&res.Total) + tx.Scopes(Paginate(page, pageSize)) + tx.Find(&res.AddressBooks) + return +} + +// Create 创建 +func (t *AddressBookService) Create(u *model.AddressBook) error { + res := global.DB.Create(u).Error + return res +} +func (t *AddressBookService) Delete(u *model.AddressBook) error { + return global.DB.Delete(u).Error +} + +// Update 更新 +func (t *AddressBookService) Update(u *model.AddressBook) error { + return global.DB.Model(u).Updates(u).Error +} diff --git a/service/group.go b/service/group.go new file mode 100644 index 0000000..358e2cb --- /dev/null +++ b/service/group.go @@ -0,0 +1,45 @@ +package service + +import ( + "Gwen/global" + "Gwen/model" + "gorm.io/gorm" +) + +type GroupService struct { +} + +// InfoById 根据用户id取用户信息 +func (us *GroupService) InfoById(id uint) *model.Group { + u := &model.Group{} + global.DB.Where("id = ?", id).First(u) + return u +} + +func (us *GroupService) List(page, pageSize uint, where func(tx *gorm.DB)) (res *model.GroupList) { + res = &model.GroupList{} + res.Page = int64(page) + res.PageSize = int64(pageSize) + tx := global.DB.Model(&model.Group{}) + if where != nil { + where(tx) + } + tx.Count(&res.Total) + tx.Scopes(Paginate(page, pageSize)) + tx.Find(&res.Groups) + return +} + +// Create 创建 +func (us *GroupService) Create(u *model.Group) error { + res := global.DB.Create(u).Error + return res +} +func (us *GroupService) Delete(u *model.Group) error { + return global.DB.Delete(u).Error +} + +// Update 更新 +func (us *GroupService) Update(u *model.Group) error { + return global.DB.Model(u).Updates(u).Error +} diff --git a/service/peer.go b/service/peer.go new file mode 100644 index 0000000..f654dc4 --- /dev/null +++ b/service/peer.go @@ -0,0 +1,63 @@ +package service + +import ( + "Gwen/global" + "Gwen/model" + "gorm.io/gorm" +) + +type PeerService struct { +} + +// FindById 根据id查找 +func (ps *PeerService) FindById(id string) *model.Peer { + p := &model.Peer{} + global.DB.Where("id = ?", id).First(p) + return p +} +func (ps *PeerService) InfoByRowId(id uint) *model.Peer { + p := &model.Peer{} + global.DB.Where("row_id = ?", id).First(p) + return p +} + +//// ListByUserIds 根据用户id取列表 +//func (ps *PeerService) ListByUserIds(userIds []uint, page, pageSize uint) (res *model.PeerList) { +// res = &model.PeerList{} +// res.Page = int64(page) +// res.PageSize = int64(pageSize) +// tx := global.DB.Model(&model.Peer{}).Preload("User") +// tx.Where("user_id in (?)", userIds) +// tx.Count(&res.Total) +// tx.Scopes(Paginate(page, pageSize)) +// tx.Find(&res.Peers) +// return +//} + +func (ps *PeerService) List(page, pageSize uint, where func(tx *gorm.DB)) (res *model.PeerList) { + res = &model.PeerList{} + res.Page = int64(page) + res.PageSize = int64(pageSize) + tx := global.DB.Model(&model.Peer{}) + if where != nil { + where(tx) + } + tx.Count(&res.Total) + tx.Scopes(Paginate(page, pageSize)) + tx.Find(&res.Peers) + return +} + +// Create 创建 +func (ps *PeerService) Create(u *model.Peer) error { + res := global.DB.Create(u).Error + return res +} +func (ps *PeerService) Delete(u *model.Peer) error { + return global.DB.Delete(u).Error +} + +// Update 更新 +func (ps *PeerService) Update(u *model.Peer) error { + return global.DB.Model(u).Updates(u).Error +} diff --git a/service/service.go b/service/service.go new file mode 100644 index 0000000..a8a4f03 --- /dev/null +++ b/service/service.go @@ -0,0 +1,42 @@ +package service + +import ( + "Gwen/model" + "gorm.io/gorm" +) + +type Service struct { + //AdminService *AdminService + //AdminRoleService *AdminRoleService + *UserService + *AddressBookService + *TagService + *PeerService + *GroupService +} + +func New() *Service { + all := new(Service) + return all +} + +var AllService = New() + +func Paginate(page, pageSize uint) func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + if page == 0 { + page = 1 + } + if pageSize == 0 { + pageSize = 10 + } + offset := (page - 1) * pageSize + return db.Offset(int(offset)).Limit(int(pageSize)) + } +} + +func CommonEnable() func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + return db.Where("status = ?", model.COMMON_STATUS_ENABLE) + } +} diff --git a/service/tag.go b/service/tag.go new file mode 100644 index 0000000..881776b --- /dev/null +++ b/service/tag.go @@ -0,0 +1,88 @@ +package service + +import ( + "Gwen/global" + "Gwen/model" + "gorm.io/gorm" +) + +type TagService struct { +} + +func (s *TagService) Info(id uint) *model.Tag { + p := &model.Tag{} + global.DB.Where("id = ?", id).First(p) + return p +} + +func (s *TagService) ListByUserId(userId uint) (res *model.TagList) { + res = s.List(1, 1000, func(tx *gorm.DB) { + tx.Where("user_id = ?", userId) + }) + return +} + +func (s *TagService) UpdateTags(userId uint, tags map[string]uint) { + tx := global.DB.Begin() + //先查询所有tag + var allTags []*model.Tag + tx.Where("user_id = ?", userId).Find(&allTags) + for _, t := range allTags { + if _, ok := tags[t.Name]; !ok { + //删除 + tx.Delete(t) + } else { + if tags[t.Name] != t.Color { + //更新 + t.Color = tags[t.Name] + tx.Save(t) + } + //移除 + delete(tags, t.Name) + } + } + //新增 + for tag, color := range tags { + t := &model.Tag{} + t.Name = tag + t.Color = color + t.UserId = userId + tx.Create(t) + } + tx.Commit() +} + +// InfoById 根据用户id取用户信息 +func (t *TagService) InfoById(id uint) *model.Tag { + u := &model.Tag{} + global.DB.Where("id = ?", id).First(u) + return u +} + +func (t *TagService) List(page, pageSize uint, where func(tx *gorm.DB)) (res *model.TagList) { + res = &model.TagList{} + res.Page = int64(page) + res.PageSize = int64(pageSize) + tx := global.DB.Model(&model.Tag{}) + if where != nil { + where(tx) + } + tx.Count(&res.Total) + tx.Scopes(Paginate(page, pageSize)) + tx.Find(&res.Tags) + return +} + +// Create 创建 +func (t *TagService) Create(u *model.Tag) error { + res := global.DB.Create(u).Error + return res +} +func (t *TagService) Delete(u *model.Tag) error { + return global.DB.Delete(u).Error +} + +// Update 更新 +func (t *TagService) Update(u *model.Tag) error { + return global.DB.Model(u).Updates(u).Error +} diff --git a/service/user.go b/service/user.go new file mode 100644 index 0000000..9b36b40 --- /dev/null +++ b/service/user.go @@ -0,0 +1,171 @@ +package service + +import ( + "Gwen/global" + adResp "Gwen/http/response/admin" + "Gwen/model" + "Gwen/utils" + "github.com/gin-gonic/gin" + "gorm.io/gorm" + "time" +) + +type UserService struct { +} + +// InfoById 根据用户id取用户信息 +func (us *UserService) InfoById(id uint) *model.User { + u := &model.User{} + global.DB.Where("id = ?", id).First(u) + return u +} + +// InfoByOpenid 根据openid取用户信息 +func (us *UserService) InfoByOpenid(openid string) *model.User { + u := &model.User{} + global.DB.Where("openid = ?", openid).First(u) + return u +} + +// InfoByUsernamePassword 根据用户名密码取用户信息 +func (us *UserService) InfoByUsernamePassword(username, password string) *model.User { + u := &model.User{} + global.DB.Where("username = ? and password = ?", username, us.EncryptPassword(password)).First(u) + return u +} + +// InfoByAccesstoken 根据accesstoken取用户信息 +func (us *UserService) InfoByAccessToken(token string) *model.User { + u := &model.User{} + ut := &model.UserToken{} + global.DB.Where("token = ?", token).First(ut) + if ut.Id == 0 { + return u + } + if ut.ExpiredAt < time.Now().Unix() { + return u + } + global.DB.Where("id = ?", ut.UserId).First(u) + return u +} + +// GenerateToken 生成token +func (us *UserService) GenerateToken(u *model.User) string { + return utils.Md5(u.Username + u.Password + time.Now().String()) +} + +// Login 登录 +func (us *UserService) Login(u *model.User) *model.UserToken { + token := us.GenerateToken(u) + ut := &model.UserToken{ + UserId: u.Id, + Token: token, + ExpiredAt: time.Now().Add(time.Hour * 24 * 7).Unix(), + } + global.DB.Create(ut) + return ut +} + +// CurUser 获取当前用户 +func (us *UserService) CurUser(c *gin.Context) *model.User { + user, _ := c.Get("curUser") + u, ok := user.(*model.User) + if !ok { + return nil + } + return u +} + +func (us *UserService) List(page, pageSize uint, where func(tx *gorm.DB)) (res *model.UserList) { + res = &model.UserList{} + res.Page = int64(page) + res.PageSize = int64(pageSize) + tx := global.DB.Model(&model.User{}) + if where != nil { + where(tx) + } + tx.Count(&res.Total) + tx.Scopes(Paginate(page, pageSize)) + tx.Find(&res.Users) + return +} + +// ListByGroupId 根据组id取用户列表 +func (us *UserService) ListByGroupId(groupId, page, pageSize uint) (res *model.UserList) { + res = us.List(page, pageSize, func(tx *gorm.DB) { + tx.Where("group_id = ?", groupId) + }) + return +} + +// ListIdsByGroupId 根据组id取用户id列表 +func (us *UserService) ListIdsByGroupId(groupId uint) (ids []uint) { + global.DB.Model(&model.User{}).Where("group_id = ?", groupId).Pluck("id", &ids) + return ids + +} + +// ListIdAndNameByGroupId 根据组id取用户id和用户名列表 +func (us *UserService) ListIdAndNameByGroupId(groupId uint) (res []*model.User) { + global.DB.Model(&model.User{}).Where("group_id = ?", groupId).Select("id, username").Find(&res) + return res +} + +// EncryptPassword 加密密码 +func (us *UserService) EncryptPassword(password string) string { + return utils.Md5(password + "rustdesk-api") +} + +// CheckUserEnable 判断用户是否禁用 +func (us *UserService) CheckUserEnable(u *model.User) bool { + return u.Status == model.COMMON_STATUS_ENABLE +} + +// Create 创建 +func (us *UserService) Create(u *model.User) error { + u.Password = us.EncryptPassword(u.Password) + res := global.DB.Create(u).Error + return res +} + +// Logout 退出登录 +func (us *UserService) Logout(u *model.User, token string) error { + return global.DB.Where("user_id = ? and token = ?", u.Id, token).Delete(&model.UserToken{}).Error +} +func (us *UserService) Delete(u *model.User) error { + return global.DB.Delete(u).Error +} + +// Update 更新 +func (us *UserService) Update(u *model.User) error { + return global.DB.Model(u).Updates(u).Error +} + +// FlushToken 清空token +func (us *UserService) FlushToken(u *model.User) error { + return global.DB.Where("user_id = ?", u.Id).Delete(&model.UserToken{}).Error +} + +// UpdatePassword 更新密码 +func (us *UserService) UpdatePassword(u *model.User, password string) error { + u.Password = us.EncryptPassword(password) + err := global.DB.Model(u).Update("password", u.Password).Error + if err != nil { + return err + } + err = us.FlushToken(u) + return err +} + +// IsAdmin 是否管理员 +func (us *UserService) IsAdmin(u *model.User) bool { + return *u.IsAdmin +} + +// RouteNames +func (us *UserService) RouteNames(u *model.User) []string { + if us.IsAdmin(u) { + return adResp.AdminRouteNames + } + return adResp.UserRouteNames +} diff --git a/utils/tools.go b/utils/tools.go new file mode 100644 index 0000000..c58e275 --- /dev/null +++ b/utils/tools.go @@ -0,0 +1,63 @@ +package utils + +import ( + "crypto/md5" + "encoding/json" + "fmt" + "reflect" + "runtime/debug" +) + +func Md5(str string) string { + t := md5.Sum(([]byte)(str)) + return fmt.Sprintf("%x", t) +} + +func CopyStructByJson(src, dst interface{}) { + str, _ := json.Marshal(src) + err := json.Unmarshal(str, dst) + if err != nil { + return + } +} + +// CopyStructToMap 结构体转map +func CopyStructToMap(src interface{}) map[string]interface{} { + var res = map[string]interface{}{} + str, _ := json.Marshal(src) + err := json.Unmarshal(str, &res) + if err != nil { + return nil + } + return res +} + +// SafeGo is a common function to recover panic for goroutines +func SafeGo(f interface{}, params ...interface{}) { + go func() { + defer func() { + if r := recover(); r != nil { + fmt.Printf("Recovered in SafeGo: %v\n", r) + debug.PrintStack() + } + }() + + // Convert f to a reflect.Value + funcValue := reflect.ValueOf(f) + + // Check if the f is a function + if funcValue.Kind() != reflect.Func { + fmt.Println("SafeGo: value is not a function") + return + } + + // Convert params to reflect.Value + paramsValue := make([]reflect.Value, len(params)) + for i, param := range params { + paramsValue[i] = reflect.ValueOf(param) + } + + // Call the function f with params + funcValue.Call(paramsValue) + }() +}