初始提交

This commit is contained in:
eryajf 2022-05-18 17:57:03 +08:00
commit 354d59ae0c
104 changed files with 7318 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
.idea
.vscode
build.sh
logs
go-web-mini
go-ldap-admin
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
tmp

13
Makefile Normal file
View File

@ -0,0 +1,13 @@
default: build
run:
GIN_MODE=release go run main.go
build:
go build -o go-ldap-admin main.go
build-linux:
CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -o go-ldap-admin main.go
build-linux-arm:
CGO_ENABLED=0 GOARCH=arm64 GOOS=linux go build -o go-ldap-admin main.go

206
README.md Normal file
View File

@ -0,0 +1,206 @@
<h1 align="center">Go-Ldap-Admin</h1>
<div align="center">
基于Go+Vue实现的openLDAP后台管理项目。
<p align="center">
<img src="https://img.shields.io/github/go-mod/go-version/eryajf-world/go-ldap-admin" alt="Go version"/>
<img src="https://img.shields.io/badge/Gin-1.6.3-brightgreen" alt="Gin version"/>
<img src="https://img.shields.io/badge/Gorm-1.20.12-brightgreen" alt="Gorm version"/>
<img src="https://img.shields.io/github/license/eryajf-world/go-ldap-admin" alt="License"/>
</p>
</div>
## 缘起
我曾经经历的公司强依赖openLDAP来作为企业内部员工管理的平台并通过openLDAP进行各平台的认证打通工作。
但成也萧何败也萧何给运维省力的同时ldap又是维护不够友好的。
在[godap](https://github.com/bradleypeabody/godap)项目中作者这样描述对ldap的感受
> The short version of the story goes like this: I hate LDAP. I used to love it. But I loved it for all the wrong reasons. LDAP is supported as an authentication solution by many different pieces of software. Aside from its de jure standard status, its wide deployment cements it as a de facto standard as well.
>
> However, just because it is a standard doesn't mean it is a great idea.
>
> I'll admit that given its age LDAP has had a good run. I'm sure its authors carefully considered how to construct the protocol and chose ASN.1 and its encoding with all of wellest of well meaning intentions.
>
> The trouble is that with today's Internet, LDAP is just a pain in the ass. You can't call it from your browser. It's not human readable or easy to debug. Tooling is often arcane and confusing. It's way more complicated than what is needed for most simple authentication-only uses. (Yes, I know there are many other uses than authentication - but it's often too complicated for those too.)
>
> Likely owing to the complexity of the protocol, there seems to be virtually no easy to use library to implement the server side of the LDAP protocol that isn't tied in with some complete directory server system; and certainly not in a language as easy to "make it work" as Go.
他说他对ldap又爱又恨因为ldap出现的最早许多的三方软件都兼容支持它它成了这方面的一个标准。但问题在于它对于维护者而言又是复杂麻烦的。就算是有Phpldapadmin这样的平台能够在浏览器维护但看到那样上古的界面以及复杂的交互逻辑仍旧能够把不少人劝退。
鉴于此我开发了这个现代化的openLDAP管理后台。
## 在线体验
> admin / 123456
演示地址:[http://go-ldap-admin.eryajf.net](http://go-ldap-admin.eryajf.net)
## 核心功能
- 基于 GIN WEB API 框架基于Casbin的 RBAC 访问控制模型JWT 认证Validator 参数校验
- 基于 GORM 的数据库存储
- 基于 go-ldap 库的主逻辑交互
- 用户管理
- 用户的增删改查
- 分组管理
- 分组的增删改查
- 分组内成员的管理
## 快速开始
你可以通过docker-compose在本地快速拉起进行体验。
快速拉起的容器包括MySQL-5.7openLDAP-1.4.0phpldapadmin-0.9.0go-ldap-admin。
服务端口映射如下:
| Service | Port |
| :-----------: | :-------------------: |
| MySQL | `3307:3306` |
| openLDAP | `389:389` |
| phpldapadmin | `8091:80` |
| go-ldap-admin | `8090:80`,`8888:8888` |
拉起之前确认是否有与本地端口冲突的情况。
```
$ git clone https://github.com/eryajf-world/go-ldap-admin.git
$ cd docs/docker-compose
$ docker-compose up -d
```
当看到容器都正常运行之后可以在本地访问http://localhost:8090用户名/密码admin/123456
`登录页:`
![](http://t.eryajf.net/imgs/2022/05/17dbe07a137c9b4c.png)
`首页:`
![](http://t.eryajf.net/imgs/2022/05/b18c5fbf5ba0e6af.png)
`用户管理:`
![](http://t.eryajf.net/imgs/2022/05/f3ae695b703c00c8.png)
`分组管理:`
![](http://t.eryajf.net/imgs/2022/05/cb7bcd851b2c972f.png)
`分组内成员管理:`
![](http://t.eryajf.net/imgs/2022/05/f1732540ce0632de.png)
## 本地开发
### 前言准备
前提是已准备好MySQL与openLDAP本地开发建议直接通过docker拉起即可可参考文档[https://wiki.eryajf.net/pages/3a0d5f](https://wiki.eryajf.net/pages/3a0d5f)。
### 拉取代码
```
# 后端代码
$ git clone https://github.com/eryajf-world/go-ldap-admin.git
# 前端代码
$ git clone https://github.com/eryajf-world/go-ldap-admin-ui.git
```
后端目录结构:
```
├─config # viper读取配置
├─controller # controller层响应路由请求的方法
├─docs # 一些物料信息
├─logic # 主要的处理逻辑
├─middleware # 中间件
├─model # 结构体模型
├─public # 一些公共的,工具类的放在这里
├─routes # 所有路由
├─service # 整合与底层存储交互的方法
├─svc # 定义入参出参的结构体
└─test # 跑测试用的
```
### 更改配置
```
# 修改后端配置
$ cd go-ldap-admin
# 文件路径 config.yml
$ vim config.yml
# 根据自己本地的情况调整数据库以及openLDAP的配置信息。
```
### 启动服务
```
# 启动后端
$ cd go-ldap-admin
$ go mod tidy
$ go run main.go
$ make run
# 启动前端
$ cd go-ldap-admin-ui
$ yarn
$ yarn dev
```
本地访问http://localhost:8090用户名/密码admin/密码是配置文件中openLDAP中admin的密码。
## 生产部署
生产环境单独部署通过Nginx代理服务配置如下
```nginx
server {
listen 80;
server_name go-ldap-admin.eryajf.net;
root /data/www/web/dist;
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control 'no-store';
}
location /api/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:8888;
}
}
```
## 感谢
感谢如下优秀的项目没有这些项目不可能会有go-ldap-admin
- 后端技术栈
- [Gin-v1.6.3](https://github.com/gin-gonic/gin)
- [Gorm-v1.20.12](https://github.com/go-gorm/gorm)
- [Go-ldap-v3.4.2](https://github.com/go-ldap/ldap)
- [Casbin-v2.22.0](https://github.com/casbin/casbin)
- 前端技术栈
- [element-ui](https://github.com/ElemeFE/element)
- [axios](https://github.com/axios/axios)
- 另外感谢
- [go-web-mini](https://github.com/gnimli/go-web-mini):项目基于该项目重构而成,感谢作者的付出。
## 另外
- 如果觉得项目不错麻烦动动小手点个⭐star⭐!
- 如果你还有其他想法或者需求欢迎在issue中交流
- 程序还有很多bug欢迎各位朋友一起协同共建

92
config.yml Normal file
View File

@ -0,0 +1,92 @@
# delelopment
system:
# 设定模式(debug/release/test,正式版改为release)
mode: debug
# url前缀
url-path-prefix: api
# 程序监听端口
port: 8888
# 是否初始化数据(没有初始数据时使用, 已发布正式版改为false)
init-data: true
# rsa公钥文件路径(config.yml相对路径, 也可以填绝对路径)
rsa-public-key: go-web-mini-pub.pem
# rsa私钥文件路径(config.yml相对路径, 也可以填绝对路径)
rsa-private-key: go-web-mini-priv.pem
logs:
# 日志等级(-1:Debug, 0:Info, 1:Warn, 2:Error, 3:DPanic, 4:Panic, 5:Fatal, -1<=level<=5, 参照zap.level源码)
level: -1
# 日志路径
path: logs
# 文件最大大小, M
max-size: 50
# 备份数
max-backups: 100
# 存放时间, 天
max-age: 30
# 是否压缩
compress: false
mysql:
# 用户名
username: root
# 密码
password: 123465
# 数据库名
database: go-ldap-admin
# 主机地址
host: localhost
# 端口
port: 3306
# 连接字56参数
query: parseTime=True&loc=Local&timeout=10000ms
# 是否打印日志
log-mode: true
# 数据库表前缀(无需再末尾添加下划线, 程序内部自动处理)
table-prefix: tb
# 编码方式
charset: utf8mb4
# 字符集(utf8mb4_general_ci速度比utf8mb4_unicode_ci快些)
collation: utf8mb4_general_ci
# casbin配置
casbin:
# 模型配置文件, config.yml相对路径
model-path: 'rbac_model.conf'
# jwt配置
jwt:
# jwt标识
realm: test jwt
# 服务端密钥
key: secret key
# token过期时间, 小时
timeout: 12000
# 刷新token最大过期时间, 小时
max-refresh: 12000
# 令牌桶限流配置
rate-limit:
# 填充一个令牌需要的时间间隔,毫秒
fill-interval: 50
# 桶容量
capacity: 200
# email configuration
email:
port: '465'
user: 'Linuxlql@163.com'
from: 'go-ldap-admin后台'
host: 'smtp.163.com'
# is-ssl: true
pass: 'your password'
# # ldap 配置
ldap:
# ldap服务器地址
ldap-url: ldap://localhost:389
ldap-base-dn: "dc=eryajf,dc=net"
ldap-admin-dn: "cn=admin,dc=eryajf,dc=net"
ldap-admin-pass: "123456"
ldap-user-dn: "ou=people,dc=eryajf,dc=net"
ldap-group-dn: "ou=group,dc=eryajf,dc=net"

144
config/config.go Normal file
View File

@ -0,0 +1,144 @@
package config
import (
"fmt"
"os"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"go.uber.org/zap/zapcore"
)
// 系统配置对应yml
// viper内置了mapstructure, yml文件用"-"区分单词, 转为驼峰方便
// 全局配置变量
var Conf = new(config)
type config struct {
System *SystemConfig `mapstructure:"system" json:"system"`
Logs *LogsConfig `mapstructure:"logs" json:"logs"`
Mysql *MysqlConfig `mapstructure:"mysql" json:"mysql"`
Casbin *CasbinConfig `mapstructure:"casbin" json:"casbin"`
Jwt *JwtConfig `mapstructure:"jwt" json:"jwt"`
RateLimit *RateLimitConfig `mapstructure:"rate-limit" json:"rateLimit"`
Ldap *LdapConfig `mapstructure:"ldap" json:"ldap"`
Email *EmailConfig `mapstructure:"email" json:"email"`
}
// 设置读取配置信息
func InitConfig() {
workDir, err := os.Getwd()
if err != nil {
panic(fmt.Errorf("读取应用目录失败:%s", err))
}
viper.SetConfigName("config")
viper.SetConfigType("yml")
viper.AddConfigPath(workDir + "/")
// 读取配置信息
err = viper.ReadInConfig()
// 热更新配置
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
// 将读取的配置信息保存至全局变量Conf
if err := viper.Unmarshal(Conf); err != nil {
panic(fmt.Errorf("初始化配置文件失败:%s \n", err))
}
// 读取rsa key
Conf.System.RSAPublicBytes = RSAReadKeyFromFile(Conf.System.RSAPublicKey)
Conf.System.RSAPrivateBytes = RSAReadKeyFromFile(Conf.System.RSAPrivateKey)
})
if err != nil {
panic(fmt.Errorf("读取配置文件失败:%s \n", err))
}
// 将读取的配置信息保存至全局变量Conf
if err := viper.Unmarshal(Conf); err != nil {
panic(fmt.Errorf("初始化配置文件失败:%s \n", err))
}
// 读取rsa key
Conf.System.RSAPublicBytes = RSAReadKeyFromFile(Conf.System.RSAPublicKey)
Conf.System.RSAPrivateBytes = RSAReadKeyFromFile(Conf.System.RSAPrivateKey)
}
// 从文件中读取RSA key
func RSAReadKeyFromFile(filename string) []byte {
f, err := os.Open(filename)
var b []byte
if err != nil {
return b
}
defer f.Close()
fileInfo, _ := f.Stat()
b = make([]byte, fileInfo.Size())
f.Read(b)
return b
}
type SystemConfig struct {
Mode string `mapstructure:"mode" json:"mode"`
UrlPathPrefix string `mapstructure:"url-path-prefix" json:"urlPathPrefix"`
Port int `mapstructure:"port" json:"port"`
InitData bool `mapstructure:"init-data" json:"initData"`
RSAPublicKey string `mapstructure:"rsa-public-key" json:"rsaPublicKey"`
RSAPrivateKey string `mapstructure:"rsa-private-key" json:"rsaPrivateKey"`
RSAPublicBytes []byte `mapstructure:"-" json:"-"`
RSAPrivateBytes []byte `mapstructure:"-" json:"-"`
}
type LogsConfig struct {
Level zapcore.Level `mapstructure:"level" json:"level"`
Path string `mapstructure:"path" json:"path"`
MaxSize int `mapstructure:"max-size" json:"maxSize"`
MaxBackups int `mapstructure:"max-backups" json:"maxBackups"`
MaxAge int `mapstructure:"max-age" json:"maxAge"`
Compress bool `mapstructure:"compress" json:"compress"`
}
type MysqlConfig struct {
Username string `mapstructure:"username" json:"username"`
Password string `mapstructure:"password" json:"password"`
Database string `mapstructure:"database" json:"database"`
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
Query string `mapstructure:"query" json:"query"`
LogMode bool `mapstructure:"log-mode" json:"logMode"`
TablePrefix string `mapstructure:"table-prefix" json:"tablePrefix"`
Charset string `mapstructure:"charset" json:"charset"`
Collation string `mapstructure:"collation" json:"collation"`
}
type CasbinConfig struct {
ModelPath string `mapstructure:"model-path" json:"modelPath"`
}
type JwtConfig struct {
Realm string `mapstructure:"realm" json:"realm"`
Key string `mapstructure:"key" json:"key"`
Timeout int `mapstructure:"timeout" json:"timeout"`
MaxRefresh int `mapstructure:"max-refresh" json:"maxRefresh"`
}
type RateLimitConfig struct {
FillInterval int64 `mapstructure:"fill-interval" json:"fillInterval"`
Capacity int64 `mapstructure:"capacity" json:"capacity"`
}
type LdapConfig struct {
LdapUrl string `mapstructure:"ldap-url" json:"ldapUrl"`
LdapBaseDN string `mapstructure:"ldap-base-dn" json:"ldapBaseDN"`
LdapAdminDN string `mapstructure:"ldap-admin-dn" json:"ldapAdminDN"`
LdapAdminPass string `mapstructure:"ldap-admin-pass" json:"ldapAdminPass"`
LdapUserDN string `mapstructure:"ldap-user-dn" json:"ldapUserDN"`
LdapGroupDN string `mapstructure:"ldap-group-dn" json:"ldapGroupDN"`
}
type EmailConfig struct {
Host string `mapstructure:"host" json:"host"`
Port string `mapstructure:"port" json:"port"`
User string `mapstructure:"user" json:"user"`
Pass string `mapstructure:"pass" json:"pass"`
From string `mapstructure:"from" json:"from"`
}

108
controller/a_controller.go Normal file
View File

@ -0,0 +1,108 @@
package controller
import (
"encoding/base64"
"fmt"
"net/http"
"regexp"
"github.com/eryajf-world/go-ldap-admin/config"
"github.com/eryajf-world/go-ldap-admin/public/tools"
"github.com/gin-gonic/gin"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
zht "github.com/go-playground/validator/v10/translations/zh"
)
var (
Api = &ApiController{}
Group = &GroupController{}
Menu = &MenuController{}
Role = &RoleController{}
User = &UserController{}
OperationLog = &OperationLogController{}
Base = &BaseController{}
validate = validator.New()
trans ut.Translator
)
func init() {
uni := ut.New(zh.New())
trans, _ = uni.GetTranslator("zh")
_ = zht.RegisterDefaultTranslations(validate, trans)
_ = validate.RegisterValidation("checkMobile", checkMobile)
}
func checkMobile(fl validator.FieldLevel) bool {
reg := `^1([38][0-9]|14[579]|5[^4]|16[6]|7[1-35-8]|9[189])\d{8}$`
rgx := regexp.MustCompile(reg)
return rgx.MatchString(fl.Field().String())
}
func Run(c *gin.Context, req interface{}, fn func() (interface{}, interface{})) {
var err error
// bind struct
err = c.Bind(req)
if err != nil {
tools.Err(c, tools.NewValidatorError(err), nil)
return
}
// 校验
err = validate.Struct(req)
if err != nil {
for _, err := range err.(validator.ValidationErrors) {
tools.Err(c, tools.NewValidatorError(fmt.Errorf(err.Translate(trans))), nil)
return
}
}
data, err1 := fn()
if err1 != nil {
tools.Err(c, tools.ReloadErr(err1), data)
return
}
tools.Success(c, data)
}
func Demo(c *gin.Context) {
CodeDebug()
c.JSON(http.StatusOK, tools.H{"code": 200, "msg": "ok", "data": "pong"})
}
func CodeDebug() {
// pass := "SI8HqZxBLGDTU5ZsL8fQyyFLKYSF3bI1KIMZ9yBqo6xWFQDk0HH7AvZUFqbiWNSPNWWNjS9TNsgS1ubTg5Lh7bV+AeSuW3cEuLN9wJI/9tg50eS94O3NETWf3RoZ2jBrd/huwcDRrNk5+cqLffUXI5Da68i1QEiQ3X1w/DW6VH4="
// fmt.Printf("秘钥为:%s\n", config.Conf.System.RSAPrivateBytes)
// // 密码通过RSA解密
// decodeData, err := tools.RSADecrypt([]byte(pass), config.Conf.System.RSAPrivateBytes)
// if err != nil {
// fmt.Printf("密码解密失败:%s\n", err)
// }
// fmt.Printf("密码解密后为:%s\n", string(decodeData))
// users, err := isql.User.GetUserByIds([]uint{1, 2, 3})
// if err != nil {
// fmt.Printf("获取用户失败:%s\n", err)
// }
// for _, user := range users {
// fmt.Println("===============", user.Username)
// }
// user, _ := isql.User.GetUserById(1)
// fmt.Println(user)
// user1 := new(model.User)
// _ = isql.User.Find(tools.H{"id": 1}, user1)
// fmt.Println(user1)
// user2, _ := isql.User.GetCurrentUser(c)
// fmt.Println("========", user2)
data := []byte("hello world")
m, _ := tools.RSAEncrypt(data, config.Conf.System.RSAPublicBytes)
fmt.Println(base64.StdEncoding.EncodeToString(m))
s, _ := tools.RSADecrypt(m, config.Conf.System.RSAPrivateBytes)
fmt.Println(string(s))
new := tools.NewGenPasswd("hello world")
fmt.Println("==========", new)
s1 := tools.NewParPasswd(new)
fmt.Println(string(s1))
}

View File

@ -0,0 +1,50 @@
package controller
import (
"github.com/eryajf-world/go-ldap-admin/logic"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"github.com/gin-gonic/gin"
)
type ApiController struct{}
// List 记录列表
func (m *ApiController) List(c *gin.Context) {
req := new(request.ApiListReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Api.List(c, req)
})
}
// GetTree 接口树
func (m *ApiController) GetTree(c *gin.Context) {
req := new(request.ApiGetTreeReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Api.GetTree(c, req)
})
}
// Add 新建记录
func (m *ApiController) Add(c *gin.Context) {
req := new(request.ApiAddReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Api.Add(c, req)
})
}
// Update 更新记录
func (m *ApiController) Update(c *gin.Context) {
req := new(request.ApiUpdateReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Api.Update(c, req)
})
}
// Delete 删除记录
func (m *ApiController) Delete(c *gin.Context) {
req := new(request.ApiDeleteReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Api.Delete(c, req)
})
}

View File

@ -0,0 +1,26 @@
package controller
import (
"github.com/eryajf-world/go-ldap-admin/logic"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"github.com/gin-gonic/gin"
)
type BaseController struct{}
// ChangePwd 用户通过邮箱修改密码
func (m *BaseController) ChangePwd(c *gin.Context) {
req := new(request.BaseChangePwdReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Base.ChangePwd(c, req)
})
}
// Dashboard 系统首页展示数据
func (m *BaseController) Dashboard(c *gin.Context) {
req := new(request.BaseDashboardReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Base.Dashboard(c, req)
})
}

View File

@ -0,0 +1,82 @@
package controller
import (
"github.com/eryajf-world/go-ldap-admin/logic"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"github.com/gin-gonic/gin"
)
type GroupController struct{}
// List 记录列表
func (m *GroupController) List(c *gin.Context) {
req := new(request.GroupListReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Group.List(c, req)
})
}
// UserInGroup 在分组内的用户
func (m *GroupController) UserInGroup(c *gin.Context) {
req := new(request.UserInGroupReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Group.UserInGroup(c, req)
})
}
// UserNoInGroup 不在分组的用户
func (m *GroupController) UserNoInGroup(c *gin.Context) {
req := new(request.UserNoInGroupReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Group.UserNoInGroup(c, req)
})
}
// // GetTree 接口树
// func (m *GroupController) GetTree(c *gin.Context) {
// req := new(request.GroupGetTreeReq)
// Run(c, req, func() (interface{}, interface{}) {
// return logic.Group.GetTree(c, req)
// })
// }
// Add 新建记录
func (m *GroupController) Add(c *gin.Context) {
req := new(request.GroupAddReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Group.Add(c, req)
})
}
// Update 更新记录
func (m *GroupController) Update(c *gin.Context) {
req := new(request.GroupUpdateReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Group.Update(c, req)
})
}
// Delete 删除记录
func (m *GroupController) Delete(c *gin.Context) {
req := new(request.GroupDeleteReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Group.Delete(c, req)
})
}
// AddUser 添加用户
func (m *GroupController) AddUser(c *gin.Context) {
req := new(request.GroupAddUserReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Group.AddUser(c, req)
})
}
// RemoveUser 移除用户
func (m *GroupController) RemoveUser(c *gin.Context) {
req := new(request.GroupRemoveUserReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Group.RemoveUser(c, req)
})
}

View File

@ -0,0 +1,50 @@
package controller
import (
"github.com/eryajf-world/go-ldap-admin/logic"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"github.com/gin-gonic/gin"
)
type MenuController struct{}
// // List 记录列表
// func (m *MenuController) List(c *gin.Context) {
// req := new(request.MenuListReq)
// Run(c, req, func() (interface{}, interface{}) {
// return logic.Menu.List(c, req)
// })
// }
// GetTree 菜单树
func (m *MenuController) GetTree(c *gin.Context) {
req := new(request.MenuGetTreeReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Menu.GetTree(c, req)
})
}
// Add 新建
func (m *MenuController) Add(c *gin.Context) {
req := new(request.MenuAddReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Menu.Add(c, req)
})
}
// Update 更新记录
func (m *MenuController) Update(c *gin.Context) {
req := new(request.MenuUpdateReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Menu.Update(c, req)
})
}
// Delete 删除记录
func (m *MenuController) Delete(c *gin.Context) {
req := new(request.MenuDeleteReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Menu.Delete(c, req)
})
}

View File

@ -0,0 +1,26 @@
package controller
import (
"github.com/eryajf-world/go-ldap-admin/logic"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"github.com/gin-gonic/gin"
)
type OperationLogController struct{}
// List 记录列表
func (m *OperationLogController) List(c *gin.Context) {
req := new(request.OperationLogListReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.OperationLog.List(c, req)
})
}
// Delete 删除记录
func (m *OperationLogController) Delete(c *gin.Context) {
req := new(request.OperationLogDeleteReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.OperationLog.Delete(c, req)
})
}

View File

@ -0,0 +1,74 @@
package controller
import (
"github.com/eryajf-world/go-ldap-admin/logic"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"github.com/gin-gonic/gin"
)
type RoleController struct{}
// List 记录列表
func (m *RoleController) List(c *gin.Context) {
req := new(request.RoleListReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Role.List(c, req)
})
}
// Add 新建
func (m *RoleController) Add(c *gin.Context) {
req := new(request.RoleAddReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Role.Add(c, req)
})
}
// Update 更新记录
func (m *RoleController) Update(c *gin.Context) {
req := new(request.RoleUpdateReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Role.Update(c, req)
})
}
// Delete 删除记录
func (m *RoleController) Delete(c *gin.Context) {
req := new(request.RoleDeleteReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Role.Delete(c, req)
})
}
// GetMenuList 获取菜单列表
func (m *RoleController) GetMenuList(c *gin.Context) {
req := new(request.RoleGetMenuListReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Role.GetMenuList(c, req)
})
}
// GetApiList 获取接口列表
func (m *RoleController) GetApiList(c *gin.Context) {
req := new(request.RoleGetApiListReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Role.GetApiList(c, req)
})
}
// UpdateMenus 更新菜单
func (m *RoleController) UpdateMenus(c *gin.Context) {
req := new(request.RoleUpdateMenusReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Role.UpdateMenus(c, req)
})
}
// UpdateApis 更新接口
func (m *RoleController) UpdateApis(c *gin.Context) {
req := new(request.RoleUpdateApisReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.Role.UpdateApis(c, req)
})
}

View File

@ -0,0 +1,66 @@
package controller
import (
"github.com/eryajf-world/go-ldap-admin/logic"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"github.com/gin-gonic/gin"
)
type UserController struct{}
// Add 添加记录
func (m *UserController) Add(c *gin.Context) {
req := new(request.UserAddReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.User.Add(c, req)
})
}
// Update 更新记录
func (m *UserController) Update(c *gin.Context) {
req := new(request.UserUpdateReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.User.Update(c, req)
})
}
// List 记录列表
func (m *UserController) List(c *gin.Context) {
req := new(request.UserListReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.User.List(c, req)
})
}
// Delete 删除记录
func (m UserController) Delete(c *gin.Context) {
req := new(request.UserDeleteReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.User.Delete(c, req)
})
}
// ChangePwd 更新密码
func (m UserController) ChangePwd(c *gin.Context) {
req := new(request.UserChangePwdReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.User.ChangePwd(c, req)
})
}
// ChangeUserStatus 更改用户状态
func (m UserController) ChangeUserStatus(c *gin.Context) {
req := new(request.UserChangeUserStatusReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.User.ChangeUserStatus(c, req)
})
}
// GetUserInfo 获取当前登录用户信息
func (uc UserController) GetUserInfo(c *gin.Context) {
req := new(request.UserGetUserInfoReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.User.GetUserInfo(c, req)
})
}

View File

@ -0,0 +1,7 @@
dn: ou=People,dc=eryajf,dc=net
objectClass: organizationalUnit
ou: people
dn: ou=Group,dc=eryajf,dc=net
objectClass: organizationalUnit
ou: group

View File

@ -0,0 +1,40 @@
[client]
port = 3306
socket = /var/lib/mysql/data/mysql.sock
[mysqld]
# 针对5.7版本执行group by字句出错问题解决
sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'
# 一般配置选项
basedir = /var/lib/mysql
datadir = /var/lib/mysql/data
port = 3306
socket = /var/lib/mysql/data/mysql.sock
lc-messages-dir = /usr/share/mysql # 务必配置此项否则执行sql出错时只能显示错误代码而不显示具体错误消息
character-set-server=utf8mb4
back_log = 300
max_connections = 3000
max_connect_errors = 50
table_open_cache = 4096
max_allowed_packet = 32M
#binlog_cache_size = 4M
max_heap_table_size = 128M
read_rnd_buffer_size = 16M
sort_buffer_size = 16M
join_buffer_size = 16M
thread_cache_size = 16
query_cache_size = 64M
query_cache_limit = 4M
ft_min_word_len = 8
thread_stack = 512K
#tx_isolation = READ-COMMITTED
tmp_table_size = 64M
#log-bin=mysql-bin
long_query_time = 6
server_id=1
innodb_buffer_pool_size = 1024M
innodb_thread_concurrency = 16
innodb_log_buffer_size = 16M
wait_timeout= 31536000
interactive_timeout= 31536000
lower_case_table_names = 1
bind-address = 0.0.0.0

View File

@ -0,0 +1,87 @@
version: '3'
networks:
go-ldap-admin:
driver: bridge
services:
mysql:
image: docker.mirrors.sjtug.sjtu.edu.cn/mysql/mysql-server:5.7
container_name: go-ldap-admin-mysql # 指定容器名称,如果不设置此参数,则由系统自动生成
hostname: go-ldap-admin-mysql
restart: always # 设置容器自启模式
ports:
- '3307:3306'
environment:
TZ: Asia/Shanghai # 设置容器时区与宿主机保持一致
MYSQL_ROOT_PASSWORD: 123456 # 设置root密码
MYSQL_ROOT_HOST: "%"
MYSQL_DATABASE: go_ldap_admin
volumes:
# 数据挂载目录自行修改哦!
- /etc/localtime:/etc/localtime:ro # 设置容器时区与宿主机保持一致
- ./data/mysql:/var/lib/mysql/data # 映射数据库保存目录到宿主机,防止数据丢失
- ./config/my.cnf:/etc/mysql/my.cnf # 映射数据库配置文件
command: --default-authentication-plugin=mysql_native_password #解决外部无法访问
networks:
- go-ldap-admin
openldap:
image: docker.mirrors.sjtug.sjtu.edu.cn/osixia/openldap:1.4.0
container_name: go-ldap-admin-openldap
hostname: go-ldap-admin-openldap
restart: always
environment:
TZ: Asia/Shanghai
LDAP_ORGANISATION: "eryajf.net"
LDAP_DOMAIN: "eryajf.net"
LDAP_ADMIN_PASSWORD: "123456"
command: [ '--copy-service' ]
volumes:
- ./data/openldap/database:/var/lib/ldap
- ./data/openldap/config:/etc/ldap/slapd.d
- ./config/init.ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom/init.ldif
ports:
- 388:389
networks:
- go-ldap-admin
phpldapadmin:
image: docker.mirrors.sjtug.sjtu.edu.cn/osixia/phpldapadmin:0.9.0
container_name: go-ldap-admin-phpldapadmin
hostname: go-ldap-admin-phpldapadmin
restart: always
environment:
TZ: Asia/Shanghai # 设置容器时区与宿主机保持一致
PHPLDAPADMIN_HTTPS: "false" # 是否使用https
PHPLDAPADMIN_LDAP_HOSTS: go-ldap-admin-openldap # 指定LDAP容器名称
ports:
- 8091:80
volumes:
- ./data/phpadmin:/var/www/phpldapadmin
depends_on:
- openldap
links:
- openldap:go-ldap-admin-openldap # ldap容器的 service_name:container_name
networks:
- go-ldap-admin
go-ldap-admin-server:
image: docker.mirrors.sjtug.sjtu.edu.cn/eryajf/go-ldap-admin
container_name: go-ldap-admin-server
hostname: go-ldap-admin-server
restart: always
environment:
TZ: Asia/Shanghai
WAIT_HOSTS: mysql:3306, openldap:389
ports:
- 8090:80
- 8888:8888
depends_on:
- mysql
- openldap
links:
- mysql:go-ldap-admin-mysql # ldap容器的 service_name:container_name
- openldap:go-ldap-admin-openldap # ldap容器的 service_name:container_name
networks:
- go-ldap-admin

10
docs/docker/Dockerfile Normal file
View File

@ -0,0 +1,10 @@
FROM eryajf/openresty:1.21.4-centos7
ADD . /
ADD default.conf /etc/nginx/conf.d/default.conf
RUN chmod +x /go-ldap-admin run.sh
EXPOSE 80
EXPOSE 8888
CMD /wait && /run.sh

12
docs/docker/README.md Normal file
View File

@ -0,0 +1,12 @@
参考https://wiki.eryajf.net/pages/95cf71/#%E5%90%AF%E7%94%A8-buildx-%E6%8F%92%E4%BB%B6
```sh
$ export DOCKER_CLI_EXPERIMENTAL=enabled
$ docker buildx create --use --name mybuilder
$ docker buildx inspect mybuilder --bootstrap
$ docker buildx build --no-cache -t eryajf/go-ldap-admin:v0.1 --platform=linux/arm64,linux/amd64 . --push
$ docker buildx build --no-cache -t eryajf/go-ldap-admin --platform=linux/arm64,linux/amd64 . --push
```

92
docs/docker/config.yml Normal file
View File

@ -0,0 +1,92 @@
# delelopment
system:
# 设定模式(debug/release/test,正式版改为release)
mode: debug
# url前缀
url-path-prefix: api
# 程序监听端口
port: 8888
# 是否初始化数据(没有初始数据时使用, 已发布正式版改为false)
init-data: true
# rsa公钥文件路径(config.yml相对路径, 也可以填绝对路径)
rsa-public-key: go-web-mini-pub.pem
# rsa私钥文件路径(config.yml相对路径, 也可以填绝对路径)
rsa-private-key: go-web-mini-priv.pem
logs:
# 日志等级(-1:Debug, 0:Info, 1:Warn, 2:Error, 3:DPanic, 4:Panic, 5:Fatal, -1<=level<=5, 参照zap.level源码)
level: -1
# 日志路径
path: logs
# 文件最大大小, M
max-size: 50
# 备份数
max-backups: 100
# 存放时间, 天
max-age: 30
# 是否压缩
compress: false
mysql:
# 用户名
username: root
# 密码
password: 123456
# 数据库名
database: go_ldap_admin
# 主机地址
host: mysql
# 端口
port: 3306
# 连接字符串参数
query: parseTime=True&loc=Local&timeout=10000ms
# 是否打印日志
log-mode: true
# 数据库表前缀(无需再末尾添加下划线, 程序内部自动处理)
table-prefix: tb
# 编码方式
charset: utf8mb4
# 字符集(utf8mb4_general_ci速度比utf8mb4_unicode_ci快些)
collation: utf8mb4_general_ci
# casbin配置
casbin:
# 模型配置文件, config.yml相对路径
model-path: 'rbac_model.conf'
# jwt配置
jwt:
# jwt标识
realm: test jwt
# 服务端密钥
key: secret key
# token过期时间, 小时
timeout: 12000
# 刷新token最大过期时间, 小时
max-refresh: 12000
# 令牌桶限流配置
rate-limit:
# 填充一个令牌需要的时间间隔,毫秒
fill-interval: 50
# 桶容量
capacity: 200
# email configuration
email:
port: '465'
user: 'Linuxlql@163.com'
from: 'go-ldap-admin后台'
host: 'smtp.163.com'
# is-ssl: true
pass: 'password'
# # ldap 配置
ldap:
# ldap服务器地址
ldap-url: ldap://openldap:389
ldap-base-dn: "dc=eryajf,dc=net"
ldap-admin-dn: "cn=admin,dc=eryajf,dc=net"
ldap-admin-pass: "123456"
ldap-user-dn: "ou=people,dc=eryajf,dc=net"
ldap-group-dn: "ou=group,dc=eryajf,dc=net"

11
docs/docker/default.conf Normal file
View File

@ -0,0 +1,11 @@
server {
listen 80;
server_name localhost;
root /dist;
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control 'no-store';
}
}

View File

@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDbOYcY8HbDaNM9ooYXoc9s+R5oR05ZL1BsVKadQBgOVH/kj7PQ
uD+ABEFVgB6rJNi287fRuZeZR+MCoG72H+AYsAhRsEaB5SuI7gDEstXuTyjhx5bz
0wUujbDK4VMgRfPO6MQo+A0c95OadDEvEQDG3KBQwLXapv+ZfsjG7NgdawIDAQAB
AoGAQqPgL3KZh5lL7YaEIJbtiQDJf4V9iZraZbPt2gtrxJ9nKUGNtbrsgqvIeIcz
y26t+h9oF3bFYLD7jwbZ9DOIWSin7NJ1RumRT/GN+i3qJfuLdTDywRG0wIiSIJR+
0jz/nG6QOW199waXMbgjTd/+FlEMfz0traqHQgIZFDkU/7ECQQD4j+/qM/922Ado
l6zvg8Z2uqEpEF0SH0l0+x8qsL2S9NjLZWgTZLiTLv3vxnA/kGCfBo/pNtskkuEx
3iTaSG8fAkEA4cjbJqcKCkxKW3gAm8OZCH9O04UzaowsHW4UsNwFkFqdoGg8q017
2W3Vc6xH4vD/1hhme+OANqyaktU4fm9kNQJBAI7g7mAKE8cU1u1ggqALd4G4NfuM
1HMeWPNNhtTbU52t8RC58eFz/EVetcmmn89qBqBi/UZpqf6UD67CqxxulrECQFXi
UkJcrbwHEw3CEvEtMOwDiRd6hnlUAn/bXLF9r/weC/F1VQaQPbkSR2xtrxaLN7XX
qDwd6Kpjc5TA2HF3q7UCQQDfTOSOmq6JJzWUFY7s5ZoVPmvPgFxqwcysgnqbP2vp
iHbNRMYI+dvj6ppC4BujGm5Wczw7vDs0/M4jREE9eY3r
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,6 @@
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDbOYcY8HbDaNM9ooYXoc9s+R5o
R05ZL1BsVKadQBgOVH/kj7PQuD+ABEFVgB6rJNi287fRuZeZR+MCoG72H+AYsAhR
sEaB5SuI7gDEstXuTyjhx5bz0wUujbDK4VMgRfPO6MQo+A0c95OadDEvEQDG3KBQ
wLXapv+ZfsjG7NgdawIDAQAB
-----END PUBLIC KEY-----

View File

@ -0,0 +1,14 @@
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && (keyMatch2(r.obj, p.obj) || keyMatch(r.obj, p.obj)) && (r.act == p.act || p.act == "*")

4
docs/docker/run.sh Normal file
View File

@ -0,0 +1,4 @@
#!/bin/bash
nginx
/go-ldap-admin

15
go-web-mini-priv.pem Normal file
View File

@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDbOYcY8HbDaNM9ooYXoc9s+R5oR05ZL1BsVKadQBgOVH/kj7PQ
uD+ABEFVgB6rJNi287fRuZeZR+MCoG72H+AYsAhRsEaB5SuI7gDEstXuTyjhx5bz
0wUujbDK4VMgRfPO6MQo+A0c95OadDEvEQDG3KBQwLXapv+ZfsjG7NgdawIDAQAB
AoGAQqPgL3KZh5lL7YaEIJbtiQDJf4V9iZraZbPt2gtrxJ9nKUGNtbrsgqvIeIcz
y26t+h9oF3bFYLD7jwbZ9DOIWSin7NJ1RumRT/GN+i3qJfuLdTDywRG0wIiSIJR+
0jz/nG6QOW199waXMbgjTd/+FlEMfz0traqHQgIZFDkU/7ECQQD4j+/qM/922Ado
l6zvg8Z2uqEpEF0SH0l0+x8qsL2S9NjLZWgTZLiTLv3vxnA/kGCfBo/pNtskkuEx
3iTaSG8fAkEA4cjbJqcKCkxKW3gAm8OZCH9O04UzaowsHW4UsNwFkFqdoGg8q017
2W3Vc6xH4vD/1hhme+OANqyaktU4fm9kNQJBAI7g7mAKE8cU1u1ggqALd4G4NfuM
1HMeWPNNhtTbU52t8RC58eFz/EVetcmmn89qBqBi/UZpqf6UD67CqxxulrECQFXi
UkJcrbwHEw3CEvEtMOwDiRd6hnlUAn/bXLF9r/weC/F1VQaQPbkSR2xtrxaLN7XX
qDwd6Kpjc5TA2HF3q7UCQQDfTOSOmq6JJzWUFY7s5ZoVPmvPgFxqwcysgnqbP2vp
iHbNRMYI+dvj6ppC4BujGm5Wczw7vDs0/M4jREE9eY3r
-----END RSA PRIVATE KEY-----

6
go-web-mini-pub.pem Normal file
View File

@ -0,0 +1,6 @@
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDbOYcY8HbDaNM9ooYXoc9s+R5o
R05ZL1BsVKadQBgOVH/kj7PQuD+ABEFVgB6rJNi287fRuZeZR+MCoG72H+AYsAhR
sEaB5SuI7gDEstXuTyjhx5bz0wUujbDK4VMgRfPO6MQo+A0c95OadDEvEQDG3KBQ
wLXapv+ZfsjG7NgdawIDAQAB
-----END PUBLIC KEY-----

73
go.mod Normal file
View File

@ -0,0 +1,73 @@
module github.com/eryajf-world/go-ldap-admin
go 1.17
require (
github.com/appleboy/gin-jwt/v2 v2.6.4
github.com/casbin/casbin/v2 v2.22.0
github.com/casbin/gorm-adapter/v3 v3.1.0
github.com/fsnotify/fsnotify v1.4.9
github.com/gin-gonic/gin v1.6.3
github.com/go-ldap/ldap/v3 v3.4.2
github.com/go-playground/locales v0.13.0
github.com/go-playground/universal-translator v0.17.0
github.com/go-playground/validator/v10 v10.4.1
github.com/juju/ratelimit v1.0.1
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/spf13/viper v1.7.1
github.com/thoas/go-funk v0.7.0
go.uber.org/zap v1.16.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gorm.io/driver/mysql v1.0.4
gorm.io/gorm v1.20.12
)
require (
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
github.com/denisenkom/go-mssqldb v0.9.0 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.1 // indirect
github.com/go-sql-driver/mysql v1.5.0 // indirect
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
github.com/golang/protobuf v1.4.3 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.8.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.0.7 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.6.2 // indirect
github.com/jackc/pgx/v4 v4.10.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.1 // indirect
github.com/json-iterator/go v1.1.10 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/magiconair/properties v1.8.4 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pelletier/go-toml v1.8.1 // indirect
github.com/spf13/afero v1.5.1 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/ugorji/go/codec v1.2.3 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
golang.org/x/text v0.3.5 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gorm.io/driver/postgres v1.0.7 // indirect
gorm.io/driver/sqlserver v1.0.6 // indirect
)

586
go.sum Normal file
View File

@ -0,0 +1,586 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/appleboy/gin-jwt/v2 v2.6.4 h1:4YlMh3AjCFnuIRiL27b7TXns7nLx8tU/TiSgh40RRUI=
github.com/appleboy/gin-jwt/v2 v2.6.4/go.mod h1:CZpq1cRw+kqi0+yD2CwVw7VGXrrx4AqBdeZnwxVmoAs=
github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4=
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/casbin/casbin/v2 v2.21.0/go.mod h1:wUgota0cQbTXE6Vd+KWpg41726jFRi7upxio0sR+Xd0=
github.com/casbin/casbin/v2 v2.22.0 h1:1duZ3Fr383ou/6KqRljYNQBw1WWfnXTwofzJ7UBLITc=
github.com/casbin/casbin/v2 v2.22.0/go.mod h1:wUgota0cQbTXE6Vd+KWpg41726jFRi7upxio0sR+Xd0=
github.com/casbin/gorm-adapter/v3 v3.1.0 h1:qYjsP40gIjQwS6/yk7x1IkHA4qWWhpB399DrYQtJbu0=
github.com/casbin/gorm-adapter/v3 v3.1.0/go.mod h1:kaMBsBHluoYwudSbVnism8LhJeVyuuqIb5nWYS/1IBU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/denisenkom/go-mssqldb v0.9.0 h1:RSohk2RsiZqLZ0zCjtfn3S4Gp4exhpBWHyQ7D0yGjAk=
github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-ldap/ldap/v3 v3.4.2 h1:zFZKcXKLqZpFMrMQGHeHWKXbDTdNCmhGY9AK41zPh+8=
github.com/go-ldap/ldap/v3 v3.4.2/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.6.4/go.mod h1:w2pne1C2tZgP+TvjqLpOigGzNqjBgQW9dUw/4Chex78=
github.com/jackc/pgconn v1.8.0 h1:FmjZ0rOyXTr1wfWs45i4a9vjnjWUAGpMuQLD9OSs+lw=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.0.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.0.7 h1:6Pwi1b3QdY65cuv6SyVO0FgPd5J3Bl7wf/nQQjinHMA=
github.com/jackc/pgproto3/v2 v2.0.7/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
github.com/jackc/pgtype v1.4.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
github.com/jackc/pgtype v1.6.2 h1:b3pDeuhbbzBYcg5kwNmNDun4pFUD/0AAr1kLXZLeNt8=
github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
github.com/jackc/pgx/v4 v4.8.1/go.mod h1:4HOLxrl8wToZJReD04/yB20GDwf4KBYETvlHciCnwW0=
github.com/jackc/pgx/v4 v4.10.1 h1:/6Q3ye4myIj6AaplUm+eRcz4OhK9HAvFf4ePsG40LJY=
github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY=
github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY=
github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.5.1 h1:VHu76Lk0LSP1x254maIu2bplkWpfBWI+B+6fdoZprcg=
github.com/spf13/afero v1.5.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/thoas/go-funk v0.7.0 h1:GmirKrs6j6zJbhJIficOsz2aAI7700KsU/5YrdHRM1Y=
github.com/thoas/go-funk v0.7.0/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=
github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.2.3 h1:WbFSXLxDFKVN69Sk8t+XHGzVCD7R8UoAATR8NqZgTbk=
github.com/ugorji/go v1.2.3/go.mod h1:5l8GZ8hZvmL4uMdy+mhCO1LjswGRYco9Q3HfuisB21A=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.3 h1:/mVYEV+Jo3IZKeA5gBngN0AvNnQltEDkR+eQikkWQu0=
github.com/ugorji/go/codec v1.2.3/go.mod h1:5FxzDJIgeiWJZslYHPj+LS1dq1ZBQVelZFnjsFGI/Uc=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM=
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.0.1/go.mod h1:KtqSthtg55lFp3S5kUXqlGaelnWpKitn4k1xZTnoiPw=
gorm.io/driver/mysql v1.0.4 h1:TATTzt+kR+IV0+h3iUB3dHUe8omCvQ0rOkmfCsUBohk=
gorm.io/driver/mysql v1.0.4/go.mod h1:MEgp8tk2n60cSBCq5iTcPDw3ns8Gs+zOva9EUhkknTs=
gorm.io/driver/postgres v1.0.1/go.mod h1:pv4dVhHvEVrP7k/UYqdBIllbdbpB5VTz89X1O0uOrCA=
gorm.io/driver/postgres v1.0.7 h1:uCVjh1w7DSZ20Duo10JadA+1a0OZpgJk/o/z8pFpNQs=
gorm.io/driver/postgres v1.0.7/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg=
gorm.io/driver/sqlserver v1.0.4/go.mod h1:ciEo5btfITTBCj9BkoUVDvgQbUdLWQNqdFY5OGuGnRg=
gorm.io/driver/sqlserver v1.0.6 h1:RKqN4qO6SZ+pAce13SoEYm7O2U/5L3F1u7V5WALvcqo=
gorm.io/driver/sqlserver v1.0.6/go.mod h1:+DhmnmNftPZOMOTkyLcs+WU5l6Q82TlTDy8skoKb5V8=
gorm.io/gorm v1.9.19/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.20.0/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.20.11/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.20.12 h1:ebZ5KrSHzet+sqOCVdH9mTjW91L298nX3v5lVxAzSUY=
gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=

19
logic/a_logic.go Normal file
View File

@ -0,0 +1,19 @@
package logic
import (
"fmt"
"github.com/eryajf-world/go-ldap-admin/public/tools"
)
var (
ReqAssertErr = tools.NewRspError(tools.SystemErr, fmt.Errorf("请求异常"))
Api = &ApiLogic{}
User = &UserLogic{}
Group = &GroupLogic{}
Role = &RoleLogic{}
Menu = &MenuLogic{}
OperationLog = &OperationLogLogic{}
Base = &BaseLogic{}
)

179
logic/api_logic.go Normal file
View File

@ -0,0 +1,179 @@
package logic
import (
"fmt"
"github.com/eryajf-world/go-ldap-admin/model"
"github.com/eryajf-world/go-ldap-admin/public/tools"
"github.com/eryajf-world/go-ldap-admin/service/isql"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"github.com/eryajf-world/go-ldap-admin/svc/response"
"github.com/gin-gonic/gin"
"github.com/thoas/go-funk"
)
type ApiLogic struct{}
// Add 添加数据
func (l ApiLogic) Add(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.ApiAddReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
// 获取当前用户
ctxUser, err := isql.User.GetCurrentLoginUser(c)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取当前登陆用户信息失败"))
}
api := model.Api{
Method: r.Method,
Path: r.Path,
Category: r.Category,
Remark: r.Remark,
Creator: ctxUser.Username,
}
// 创建接口
err = isql.Api.Add(&api)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("创建接口失败: %s", err.Error()))
}
return nil, nil
}
// List 数据列表
func (l ApiLogic) List(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.ApiListReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
// 获取数据列表
apis, err := isql.Api.List(r)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取接口列表失败: %s", err.Error()))
}
rets := make([]model.Api, 0)
for _, api := range apis {
rets = append(rets, *api)
}
count, err := isql.Api.Count()
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取接口总数失败"))
}
return response.ApiListRsp{
Total: count,
Apis: rets,
}, nil
}
// GetTree 数据树
func (l ApiLogic) GetTree(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.ApiGetTreeReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
_ = r
apis, err := isql.Api.ListAll()
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取资源列表失败: " + err.Error()))
}
// 获取所有的分类
var categoryList []string
for _, api := range apis {
categoryList = append(categoryList, api.Category)
}
// 获取去重后的分类
categoryUniq := funk.UniqString(categoryList)
apiTree := make([]*response.ApiTreeRsp, len(categoryUniq))
for i, category := range categoryUniq {
apiTree[i] = &response.ApiTreeRsp{
ID: -i,
Remark: category,
Category: category,
Children: nil,
}
for _, api := range apis {
if category == api.Category {
apiTree[i].Children = append(apiTree[i].Children, api)
}
}
}
return apiTree, nil
}
// Update 更新数据
func (l ApiLogic) Update(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.ApiUpdateReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
filter := tools.H{"id": int(r.ID)}
if !isql.Api.Exist(filter) {
return nil, tools.NewMySqlError(fmt.Errorf("接口不存在"))
}
// 获取当前登陆用户
ctxUser, err := isql.User.GetCurrentLoginUser(c)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取当前登陆用户失败"))
}
oldData := new(model.Api)
err = isql.Api.Find(filter, oldData)
if err != nil {
return nil, tools.NewMySqlError(err)
}
api := model.Api{
Model: oldData.Model,
Method: r.Method,
Path: r.Path,
Category: r.Category,
Remark: r.Remark,
Creator: ctxUser.Username,
}
err = isql.Api.Update(&api)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("更新接口失败: %s", err.Error()))
}
return nil, nil
}
// Delete 删除数据
func (l ApiLogic) Delete(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.ApiDeleteReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
for _, id := range r.ApiIds {
filter := tools.H{"id": int(id)}
if !isql.Api.Exist(filter) {
return nil, tools.NewMySqlError(fmt.Errorf("接口不存在"))
}
}
// 删除接口
err := isql.Api.Delete(r.ApiIds)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("删除接口失败: %s", err.Error()))
}
return nil, nil
}

131
logic/base_logic.go Normal file
View File

@ -0,0 +1,131 @@
package logic
import (
"fmt"
"github.com/eryajf-world/go-ldap-admin/model"
"github.com/eryajf-world/go-ldap-admin/public/tools"
"github.com/eryajf-world/go-ldap-admin/service/ildap"
"github.com/eryajf-world/go-ldap-admin/service/isql"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"github.com/eryajf-world/go-ldap-admin/svc/response"
"github.com/gin-gonic/gin"
)
type BaseLogic struct{}
// Add 添加数据
func (l BaseLogic) ChangePwd(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.BaseChangePwdReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
// 判断邮箱是否正确
if !isql.User.Exist(tools.H{"mail": r.Mail}) {
return nil, tools.NewValidatorError(fmt.Errorf("邮箱不存在,请检查邮箱是否正确"))
}
user := new(model.User)
err := isql.User.Find(tools.H{"mail": r.Mail}, user)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("通过邮箱查询用户失败" + err.Error()))
}
newpass, err := ildap.User.NewPwd(user.Username)
if err != nil {
return nil, tools.NewLdapError(fmt.Errorf("LDAP生成新密码失败" + err.Error()))
}
err = tools.SendMail([]string{user.Mail}, newpass)
if err != nil {
return nil, tools.NewLdapError(fmt.Errorf("发送邮件失败" + err.Error()))
}
// 更新数据库密码
err = isql.User.ChangePwd(user.Username, tools.NewGenPasswd(newpass))
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("在MySQL更新密码失败: " + err.Error()))
}
return nil, nil
}
// Dashboard 仪表盘
func (l BaseLogic) Dashboard(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
_, ok := req.(*request.BaseDashboardReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
userCount, err := isql.User.Count()
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取用户总数失败"))
}
groupCount, err := isql.Group.Count()
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取分组总数失败"))
}
roleCount, err := isql.Role.Count()
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取角色总数失败"))
}
menuCount, err := isql.Menu.Count()
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取菜单总数失败"))
}
apiCount, err := isql.Api.Count()
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取接口总数失败"))
}
logCount, err := isql.OperationLog.Count()
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取日志总数失败"))
}
rst := make([]*response.DashboardList, 0)
rst = append(rst,
&response.DashboardList{
DataType: "user",
DataName: "用户",
DataCount: userCount,
Icon: "people",
},
&response.DashboardList{
DataType: "group",
DataName: "分组",
DataCount: groupCount,
Icon: "peoples",
},
&response.DashboardList{
DataType: "role",
DataName: "角色",
DataCount: roleCount,
Icon: "eye-open",
},
&response.DashboardList{
DataType: "menu",
DataName: "菜单",
DataCount: menuCount,
Icon: "tree-table",
},
&response.DashboardList{
DataType: "api",
DataName: "接口",
DataCount: apiCount,
Icon: "tree",
},
&response.DashboardList{
DataType: "log",
DataName: "日志",
DataCount: logCount,
Icon: "documentation",
},
)
return rst, nil
}

357
logic/group_logic.go Normal file
View File

@ -0,0 +1,357 @@
package logic
import (
"fmt"
"strings"
"github.com/eryajf-world/go-ldap-admin/model"
"github.com/eryajf-world/go-ldap-admin/public/tools"
"github.com/eryajf-world/go-ldap-admin/service/ildap"
"github.com/eryajf-world/go-ldap-admin/service/isql"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"github.com/eryajf-world/go-ldap-admin/svc/response"
"github.com/gin-gonic/gin"
)
type GroupLogic struct{}
// Add 添加数据
func (l GroupLogic) Add(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.GroupAddReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
if isql.Group.Exist(tools.H{"group_name": r.GroupName}) {
return nil, tools.NewValidatorError(fmt.Errorf("组名已存在"))
}
// 获取当前用户
ctxUser, err := isql.User.GetCurrentLoginUser(c)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取当前登陆用户信息失败"))
}
group := model.Group{
GroupName: r.GroupName,
Remark: r.Remark,
Creator: ctxUser.Username,
}
err = ildap.Group.Add(&group)
if err != nil {
return nil, tools.NewLdapError(fmt.Errorf("向LDAP创建分组失败" + err.Error()))
}
// 创建
err = isql.Group.Add(&group)
if err != nil {
return nil, tools.NewLdapError(fmt.Errorf("向MySQL创建分组失败"))
}
// 默认创建分组之后需要将admin添加到分组中
adminInfo := new(model.User)
err = isql.User.Find(tools.H{"id": 1}, adminInfo)
if err != nil {
return nil, tools.NewMySqlError(err)
}
err = isql.Group.AddUserToGroup(&group, []model.User{*adminInfo})
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("添加用户到分组失败: %s", err.Error()))
}
return nil, nil
}
// List 数据列表
func (l GroupLogic) List(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.GroupListReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
// 获取数据列表
groups, err := isql.Group.List(r)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取分组列表失败: %s", err.Error()))
}
rets := make([]model.Group, 0)
for _, group := range groups {
rets = append(rets, *group)
}
count, err := isql.Group.Count()
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取分组总数失败"))
}
return response.GroupListRsp{
Total: count,
Groups: rets,
}, nil
}
// Update 更新数据
func (l GroupLogic) Update(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.GroupUpdateReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
filter := tools.H{"id": int(r.ID)}
if !isql.Group.Exist(filter) {
return nil, tools.NewMySqlError(fmt.Errorf("分组不存在"))
}
// 获取当前登陆用户
ctxUser, err := isql.User.GetCurrentLoginUser(c)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取当前登陆用户失败"))
}
oldData := new(model.Group)
err = isql.Group.Find(filter, oldData)
if err != nil {
return nil, tools.NewMySqlError(err)
}
group := model.Group{
Model: oldData.Model,
GroupName: oldData.GroupName,
Remark: r.Remark,
Creator: ctxUser.Username,
}
err = ildap.Group.Update(&group)
if err != nil {
return nil, tools.NewLdapError(fmt.Errorf("向LDAP更新分组失败"))
}
err = isql.Group.Update(&group)
if err != nil {
return nil, tools.NewLdapError(fmt.Errorf("向MySQL更新分组失败"))
}
return nil, nil
}
// Delete 删除数据
func (l GroupLogic) Delete(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.GroupDeleteReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
for _, id := range r.GroupIds {
filter := tools.H{"id": int(id)}
if !isql.Group.Exist(filter) {
return nil, tools.NewMySqlError(fmt.Errorf("分组不存在"))
}
}
groups, err := isql.Group.GetGroupByIds(r.GroupIds)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取分组列表失败: %s", err.Error()))
}
for _, group := range groups {
err := ildap.Group.Delete(group.GroupName)
if err != nil {
return nil, tools.NewLdapError(fmt.Errorf("向LDAP删除分组失败"))
}
}
// 删除接口
err = isql.Group.Delete(r.GroupIds)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("删除接口失败: %s", err.Error()))
}
return nil, nil
}
// AddUser 添加用户到分组
func (l GroupLogic) AddUser(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.GroupAddUserReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
filter := tools.H{"id": r.GroupID}
if !isql.Group.Exist(filter) {
return nil, tools.NewMySqlError(fmt.Errorf("分组不存在"))
}
users, err := isql.User.GetUserByIds(r.UserIds)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取用户列表失败: %s", err.Error()))
}
group := new(model.Group)
err = isql.Group.Find(filter, group)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取分组失败: %s", err.Error()))
}
for _, user := range users {
err := ildap.Group.AddUserToGroup(group.GroupName, user.Username)
if err != nil {
return nil, tools.NewLdapError(fmt.Errorf("向LDAP添加用户到分组失败" + err.Error()))
}
}
err = isql.Group.AddUserToGroup(group, users)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("添加用户到分组失败: %s", err.Error()))
}
return nil, nil
}
// RemoveUser 移除用户
func (l GroupLogic) RemoveUser(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.GroupRemoveUserReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
filter := tools.H{"id": r.GroupID}
if !isql.Group.Exist(filter) {
return nil, tools.NewMySqlError(fmt.Errorf("分组不存在"))
}
users, err := isql.User.GetUserByIds(r.UserIds)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取用户列表失败: %s", err.Error()))
}
group := new(model.Group)
err = isql.Group.Find(filter, group)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取分组失败: %s", err.Error()))
}
for _, user := range users {
err := ildap.Group.RemoveUserFromGroup(group.GroupName, user.Username)
if err != nil {
return nil, tools.NewLdapError(fmt.Errorf("将用户从ldap移除失败" + err.Error()))
}
}
err = isql.Group.RemoveUserFromGroup(group, users)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("将用户从MySQL移除失败: %s", err.Error()))
}
return nil, nil
}
// UserInGroup 在分组内的用户
func (l GroupLogic) UserInGroup(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.UserInGroupReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
filter := tools.H{"id": r.GroupID}
if !isql.Group.Exist(filter) {
return nil, tools.NewMySqlError(fmt.Errorf("分组不存在"))
}
group := new(model.Group)
err := isql.Group.Find(filter, group)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取分组失败: %s", err.Error()))
}
rets := make([]response.Guser, 0)
for _, user := range group.Users {
if r.Nickname != "" && !strings.Contains(user.Nickname, r.Nickname) {
continue
}
rets = append(rets, response.Guser{
UserId: int64(user.ID),
UserName: user.Username,
NickName: user.Nickname,
Mail: user.Mail,
JobNumber: user.JobNumber,
Mobile: user.Mobile,
Introduction: user.Introduction,
})
}
return response.GroupUsers{
GroupId: int64(group.ID),
GroupName: group.GroupName,
GroupRemark: group.Remark,
UserList: rets,
}, nil
}
// UserNoInGroup 不在分组内的用户
func (l GroupLogic) UserNoInGroup(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.UserNoInGroupReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
filter := tools.H{"id": r.GroupID}
if !isql.Group.Exist(filter) {
return nil, tools.NewMySqlError(fmt.Errorf("分组不存在"))
}
group := new(model.Group)
err := isql.Group.Find(filter, group)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取分组失败: %s", err.Error()))
}
var userList []*model.User
userList, err = isql.User.ListAll()
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取资源列表失败: " + err.Error()))
}
rets := make([]response.Guser, 0)
for _, user := range userList {
in := true
for _, groupUser := range group.Users {
if user.Username == groupUser.Username {
in = false
break
}
}
if in {
if r.Nickname != "" && !strings.Contains(user.Nickname, r.Nickname) {
continue
}
rets = append(rets, response.Guser{
UserId: int64(user.ID),
UserName: user.Username,
NickName: user.Nickname,
Mail: user.Mail,
JobNumber: user.JobNumber,
Mobile: user.Mobile,
Introduction: user.Introduction,
})
}
}
return response.GroupUsers{
GroupId: int64(group.ID),
GroupName: group.GroupName,
GroupRemark: group.Remark,
UserList: rets,
}, nil
}

181
logic/menu_logic.go Normal file
View File

@ -0,0 +1,181 @@
package logic
import (
"fmt"
"github.com/eryajf-world/go-ldap-admin/model"
"github.com/eryajf-world/go-ldap-admin/public/tools"
"github.com/eryajf-world/go-ldap-admin/service/isql"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"github.com/gin-gonic/gin"
)
type MenuLogic struct{}
// Add 添加数据
func (l MenuLogic) Add(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.MenuAddReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
if isql.Menu.Exist(tools.H{"name": r.Name}) {
return nil, tools.NewMySqlError(fmt.Errorf("菜单名称已存在"))
}
// 获取当前用户
ctxUser, err := isql.User.GetCurrentLoginUser(c)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取当前登陆用户信息失败"))
}
menu := model.Menu{
Name: r.Name,
Title: r.Title,
Icon: r.Icon,
Path: r.Path,
Redirect: r.Redirect,
Component: r.Component,
Sort: r.Sort,
Status: r.Status,
Hidden: r.Hidden,
NoCache: r.NoCache,
AlwaysShow: r.AlwaysShow,
Breadcrumb: r.Breadcrumb,
ActiveMenu: r.ActiveMenu,
ParentId: r.ParentId,
Creator: ctxUser.Username,
}
err = isql.Menu.Add(&menu)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("创建记录失败: %s", err.Error()))
}
return nil, nil
}
// // List 数据列表
// func (l MenuLogic) List(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
// _, ok := req.(*request.MenuListReq)
// if !ok {
// return nil, ReqAssertErr
// }
// _ = c
// menus, err := isql.Menu.List()
// if err != nil {
// return nil, tools.NewMySqlError(fmt.Errorf("获取资源列表失败: %s", err.Error()))
// }
// rets := make([]model.Menu, 0)
// for _, menu := range menus {
// rets = append(rets, *menu)
// }
// count, err := isql.Menu.Count()
// if err != nil {
// return nil, tools.NewMySqlError(fmt.Errorf("获取资源总数失败"))
// }
// return response.MenuListRsp{
// Total: count,
// Menus: rets,
// }, nil
// }
// Update 更新数据
func (l MenuLogic) Update(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.MenuUpdateReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
filter := tools.H{"id": int(r.ID)}
if !isql.Menu.Exist(filter) {
return nil, tools.NewMySqlError(fmt.Errorf("改ID对应的记录不存在"))
}
// 获取当前登陆用户
ctxUser, err := isql.User.GetCurrentLoginUser(c)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取当前登陆用户失败"))
}
oldData := new(model.Menu)
err = isql.Menu.Find(filter, oldData)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取记录失败: %s", err.Error()))
}
menu := model.Menu{
Model: oldData.Model,
Name: r.Name,
Title: r.Title,
Icon: r.Icon,
Path: r.Path,
Redirect: r.Redirect,
Component: r.Component,
Sort: r.Sort,
Status: r.Status,
Hidden: r.Hidden,
NoCache: r.NoCache,
AlwaysShow: r.AlwaysShow,
Breadcrumb: r.Breadcrumb,
ActiveMenu: r.ActiveMenu,
ParentId: r.ParentId,
Creator: ctxUser.Username,
}
err = isql.Menu.Update(&menu)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("更新记录失败: %s", err.Error()))
}
return nil, nil
}
// Delete 删除数据
func (l MenuLogic) Delete(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.MenuDeleteReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
for _, id := range r.MenuIds {
filter := tools.H{"id": int(id)}
if !isql.Menu.Exist(filter) {
return nil, tools.NewMySqlError(fmt.Errorf("改ID对应的记录不存在"))
}
}
// 删除接口
err := isql.Menu.Delete(r.MenuIds)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("删除接口失败: %s", err.Error()))
}
return nil, nil
}
// GetTree 数据树
func (l MenuLogic) GetTree(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
_, ok := req.(*request.MenuGetTreeReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
var menus []*model.Menu
menus, err := isql.Menu.List()
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取资源列表失败: " + err.Error()))
}
tree := isql.GenMenuTree(0, menus)
return tree, nil
}

View File

@ -0,0 +1,74 @@
package logic
import (
"fmt"
"github.com/eryajf-world/go-ldap-admin/model"
"github.com/eryajf-world/go-ldap-admin/public/tools"
"github.com/eryajf-world/go-ldap-admin/service/isql"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"github.com/eryajf-world/go-ldap-admin/svc/response"
"github.com/gin-gonic/gin"
)
type OperationLogLogic struct{}
// List 数据列表
func (l OperationLogLogic) List(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.OperationLogListReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
// 获取数据列表
logs, err := isql.OperationLog.List(r)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取接口列表失败: %s", err.Error()))
}
rets := make([]model.OperationLog, 0)
for _, log := range logs {
rets = append(rets, *log)
}
count, err := isql.OperationLog.Count()
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取接口总数失败"))
}
return response.LogListRsp{
Total: count,
Logs: rets,
}, nil
// 获取
// logs, err := isql.OperationLog.List(&r)
// if err != nil {
// response.Fail(c, nil, "获取操作日志列表失败: "+err.Error())
// return
// }
// return nil, nil
}
// Delete 删除数据
func (l OperationLogLogic) Delete(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.OperationLogDeleteReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
for _, id := range r.OperationLogIds {
filter := tools.H{"id": int(id)}
if !isql.OperationLog.Exist(filter) {
return nil, tools.NewMySqlError(fmt.Errorf("改条记录不存在"))
}
}
// 删除接口
err := isql.OperationLog.Delete(r.OperationLogIds)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("删除改条记录失败: %s", err.Error()))
}
return nil, nil
}

437
logic/role_logic.go Normal file
View File

@ -0,0 +1,437 @@
package logic
import (
"fmt"
"github.com/eryajf-world/go-ldap-admin/model"
"github.com/eryajf-world/go-ldap-admin/public/common"
"github.com/eryajf-world/go-ldap-admin/public/tools"
"github.com/eryajf-world/go-ldap-admin/service/isql"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"github.com/eryajf-world/go-ldap-admin/svc/response"
"github.com/gin-gonic/gin"
"github.com/thoas/go-funk"
)
type RoleLogic struct{}
// Add 添加数据
func (l RoleLogic) Add(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.RoleAddReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
if isql.Role.Exist(tools.H{"name": r.Name}) {
return nil, tools.NewValidatorError(fmt.Errorf("该角色名已存在"))
}
// 获取当前用户最高角色等级
minSort, ctxUser, err := isql.User.GetCurrentUserMinRoleSort(c)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取当前用户最高角色等级失败: %s", err.Error()))
}
if minSort != 1 {
return nil, tools.NewValidatorError(fmt.Errorf("当前用户没有权限更新角色"))
}
// 用户不能创建比自己等级高或相同等级的角色
if minSort >= r.Sort {
return nil, tools.NewValidatorError(fmt.Errorf("不能创建比自己等级高或相同等级的角色"))
}
role := model.Role{
Name: r.Name,
Keyword: r.Keyword,
Remark: r.Remark,
Status: r.Status,
Sort: r.Sort,
Creator: ctxUser.Username,
}
// 创建角色
err = isql.Role.Add(&role)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("创建角色失败: %s", err.Error()))
}
return nil, nil
}
// List 数据列表
func (l RoleLogic) List(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.RoleListReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
// 获取数据列表
roles, err := isql.Role.List(r)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取菜单列表失败: %s", err.Error()))
}
count, err := isql.Role.Count()
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取接口总数失败"))
}
rets := make([]model.Role, 0)
for _, role := range roles {
rets = append(rets, *role)
}
return response.RoleListRsp{
Total: count,
Roles: rets,
}, nil
}
// Update 更新数据
func (l RoleLogic) Update(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.RoleUpdateReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
filter := tools.H{"id": r.ID}
if !isql.Role.Exist(filter) {
return nil, tools.NewValidatorError(fmt.Errorf("该角色名不已存在"))
}
// 当前用户角色排序最小值(最高等级角色)以及当前用户
minSort, ctxUser, err := isql.User.GetCurrentUserMinRoleSort(c)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取当前用户最高角色等级失败: %s", err.Error()))
}
if minSort != 1 {
return nil, tools.NewValidatorError(fmt.Errorf("当前用户没有权限更新角色"))
}
// 不能更新比自己角色等级高或相等的角色
// 根据path中的角色ID获取该角色信息
roles, err := isql.Role.GetRolesByIds([]uint{r.ID})
if err != nil || len(roles) == 0 {
return nil, tools.NewMySqlError(fmt.Errorf("获取角色信息失败: %s", err.Error()))
}
if minSort >= roles[0].Sort {
return nil, tools.NewValidatorError(fmt.Errorf("不能更新比自己角色等级高或相等的角色"))
}
// 不能把角色等级更新得比当前用户的等级高
if minSort >= r.Sort {
return nil, tools.NewValidatorError(fmt.Errorf("不能把角色等级更新得比当前用户的等级高或相同"))
}
oldData := new(model.Role)
err = isql.Role.Find(filter, oldData)
if err != nil {
return nil, tools.NewMySqlError(err)
}
role := model.Role{
Model: oldData.Model,
Name: r.Name,
Keyword: r.Keyword,
Remark: r.Remark,
Status: r.Status,
Sort: r.Sort,
Creator: ctxUser.Username,
}
// 更新角色
err = isql.Role.Update(&role)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("更新角色失败: %s", err.Error()))
}
// 如果更新成功且更新了角色的keyword, 则更新casbin中policy
if r.Keyword != roles[0].Keyword {
// 获取policy
rolePolicies := common.CasbinEnforcer.GetFilteredPolicy(0, roles[0].Keyword)
if len(rolePolicies) == 0 {
return
}
rolePoliciesCopy := make([][]string, 0)
// 替换keyword
for _, policy := range rolePolicies {
policyCopy := make([]string, len(policy))
copy(policyCopy, policy)
rolePoliciesCopy = append(rolePoliciesCopy, policyCopy)
policy[0] = r.Keyword
}
//gormadapter未实现UpdatePolicies方法等gorm更新---
//isUpdated, _ := common.CasbinEnforcer.UpdatePolicies(rolePoliciesCopy, rolePolicies)
//if !isUpdated {
// response.Fail(c, nil, "更新角色成功,但角色关键字关联的权限接口更新失败!")
// return
//}
// 这里需要先新增再删除(先删除再增加会出错)
isAdded, _ := common.CasbinEnforcer.AddPolicies(rolePolicies)
if !isAdded {
return nil, tools.NewOperationError(fmt.Errorf("更新角色成功,但角色关键字关联的权限接口更新失败"))
}
isRemoved, _ := common.CasbinEnforcer.RemovePolicies(rolePoliciesCopy)
if !isRemoved {
return nil, tools.NewOperationError(fmt.Errorf("更新角色成功,但角色关键字关联的权限接口更新失败"))
}
err := common.CasbinEnforcer.LoadPolicy()
if err != nil {
return nil, tools.NewOperationError(fmt.Errorf("更新角色成功,但角色关键字关联角色的权限接口策略加载失败"))
}
}
// 更新角色成功处理用户信息缓存有两种做法:(这里使用第二种方法,因为一个角色下用户数量可能很多,第二种方法可以分散数据库压力)
// 1.可以帮助用户更新拥有该角色的用户信息缓存,使用下面方法
// err = ur.UpdateUserInfoCacheByRoleId(uint(roleId))
// 2.直接清理缓存,让活跃的用户自己重新缓存最新用户信息
isql.User.ClearUserInfoCache()
return nil, nil
}
// Delete 删除数据
func (l RoleLogic) Delete(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.RoleDeleteReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
// 获取当前登陆用户最高等级角色
minSort, _, err := isql.User.GetCurrentUserMinRoleSort(c)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取当前用户最高角色等级失败: %s", err.Error()))
}
// 获取角色信息
roles, err := isql.Role.GetRolesByIds(r.RoleIds)
if err != nil || len(roles) == 0 {
return nil, tools.NewMySqlError(fmt.Errorf("获取角色信息失败: %s", err.Error()))
}
// 不能删除比自己角色等级高或相等的角色
for _, role := range roles {
if minSort >= role.Sort {
return nil, tools.NewValidatorError(fmt.Errorf("不能删除比自己角色等级高或相等的角色"))
}
}
// 删除角色
err = isql.Role.Delete(r.RoleIds)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("删除角色失败: %s", err.Error()))
}
// 删除角色成功直接清理缓存,让活跃的用户自己重新缓存最新用户信息
isql.User.ClearUserInfoCache()
return nil, nil
}
// GetMenuList 获取角色菜单列表
func (l RoleLogic) GetMenuList(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.RoleGetMenuListReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
menus, err := isql.Role.GetRoleMenusById(r.RoleID)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取角色的权限菜单失败: " + err.Error()))
}
return menus, nil
}
// GetApiList 获取角色接口列表
func (l RoleLogic) GetApiList(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.RoleGetApiListReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
role := new(model.Role)
err := isql.Role.Find(tools.H{"id": r.RoleID}, role)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取资源失败: " + err.Error()))
}
policies := common.CasbinEnforcer.GetFilteredPolicy(0, role.Keyword)
apis, err := isql.Api.ListAll()
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取资源列表失败: " + err.Error()))
}
accessApis := make([]*model.Api, 0)
for _, policy := range policies {
path := policy[1]
method := policy[2]
for _, api := range apis {
if path == api.Path && method == api.Method {
accessApis = append(accessApis, api)
break
}
}
}
return accessApis, nil
}
// UpdateMenus 更新角色菜单
func (l RoleLogic) UpdateMenus(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.RoleUpdateMenusReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
roles, err := isql.Role.GetRolesByIds([]uint{r.RoleID})
if err != nil || len(roles) == 0 {
return nil, tools.NewMySqlError(fmt.Errorf("未获取到角色信息"))
}
// 当前用户角色排序最小值(最高等级角色)以及当前用户
minSort, ctxUser, err := isql.User.GetCurrentUserMinRoleSort(c)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取当前用户最高角色等级失败: %s", err.Error()))
}
// (非管理员)不能更新比自己角色等级高或相等角色的权限菜单
if minSort != 1 {
if minSort >= roles[0].Sort {
return nil, tools.NewValidatorError(fmt.Errorf("不能更新比自己角色等级高或相等角色的权限菜单"))
}
}
// 获取当前用户所拥有的权限菜单
ctxUserMenus, err := isql.Menu.GetUserMenusByUserId(ctxUser.ID)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取当前用户的可访问菜单列表失败: " + err.Error()))
}
// 获取当前用户所拥有的权限菜单ID
ctxUserMenusIds := make([]uint, 0)
for _, menu := range ctxUserMenus {
ctxUserMenusIds = append(ctxUserMenusIds, menu.ID)
}
// 用户需要修改的菜单集合
reqMenus := make([]*model.Menu, 0)
// (非管理员)不能把角色的权限菜单设置的比当前用户所拥有的权限菜单多
if minSort != 1 {
for _, id := range r.MenuIds {
if !funk.Contains(ctxUserMenusIds, id) {
return nil, tools.NewValidatorError(fmt.Errorf("无权设置ID为%d的菜单", id))
}
}
for _, id := range r.MenuIds {
for _, menu := range ctxUserMenus {
if id == menu.ID {
reqMenus = append(reqMenus, menu)
break
}
}
}
} else {
// 管理员随意设置
// 根据menuIds查询查询菜单
menus, err := isql.Menu.List()
if err != nil {
return nil, tools.NewValidatorError(fmt.Errorf("获取菜单列表失败: " + err.Error()))
}
for _, menuId := range r.MenuIds {
for _, menu := range menus {
if menuId == menu.ID {
reqMenus = append(reqMenus, menu)
}
}
}
}
roles[0].Menus = reqMenus
err = isql.Role.UpdateRoleMenus(roles[0])
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("更新角色的权限菜单失败: " + err.Error()))
}
return nil, nil
}
// UpdateApis 更新角色接口
func (l RoleLogic) UpdateApis(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.RoleUpdateApisReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
// 根据path中的角色ID获取该角色信息
roles, err := isql.Role.GetRolesByIds([]uint{r.RoleID})
if err != nil || len(roles) == 0 {
return nil, tools.NewMySqlError(fmt.Errorf("未获取到角色信息"))
}
// 当前用户角色排序最小值(最高等级角色)以及当前用户
minSort, ctxUser, err := isql.User.GetCurrentUserMinRoleSort(c)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取当前用户最高角色等级失败: %s", err.Error()))
}
// (非管理员)不能更新比自己角色等级高或相等角色的权限菜单
if minSort != 1 {
if minSort >= roles[0].Sort {
return nil, tools.NewValidatorError(fmt.Errorf("不能更新比自己角色等级高或相等角色的权限菜单"))
}
}
// 获取当前用户所拥有的权限接口
ctxRoles := ctxUser.Roles
ctxRolesPolicies := make([][]string, 0)
for _, role := range ctxRoles {
policy := common.CasbinEnforcer.GetFilteredPolicy(0, role.Keyword)
ctxRolesPolicies = append(ctxRolesPolicies, policy...)
}
// 得到path中的角色ID对应角色能够设置的权限接口集合
for _, policy := range ctxRolesPolicies {
policy[0] = roles[0].Keyword
}
// 根据apiID获取接口详情
apis, err := isql.Api.GetApisById(r.ApiIds)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("根据接口ID获取接口信息失败"))
}
// 生成前端想要设置的角色policies
reqRolePolicies := make([][]string, 0)
for _, api := range apis {
reqRolePolicies = append(reqRolePolicies, []string{
roles[0].Keyword, api.Path, api.Method,
})
}
// (非管理员)不能把角色的权限接口设置的比当前用户所拥有的权限接口多
if minSort != 1 {
for _, reqPolicy := range reqRolePolicies {
if !funk.Contains(ctxRolesPolicies, reqPolicy) {
return nil, tools.NewValidatorError(fmt.Errorf("无权设置路径为%s,请求方式为%s的接口", reqPolicy[1], reqPolicy[2]))
}
}
}
// 更新角色的权限接口
err = isql.Role.UpdateRoleApis(roles[0].Keyword, reqRolePolicies)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("更新角色的权限接口失败"))
}
return nil, nil
}

418
logic/user_logic.go Normal file
View File

@ -0,0 +1,418 @@
package logic
import (
"fmt"
"github.com/eryajf-world/go-ldap-admin/config"
"github.com/eryajf-world/go-ldap-admin/model"
"github.com/eryajf-world/go-ldap-admin/public/tools"
"github.com/eryajf-world/go-ldap-admin/service/ildap"
"github.com/eryajf-world/go-ldap-admin/service/isql"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"github.com/eryajf-world/go-ldap-admin/svc/response"
"github.com/gin-gonic/gin"
"github.com/thoas/go-funk"
)
type UserLogic struct{}
// Add 添加数据
func (l UserLogic) Add(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.UserAddReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
if isql.User.Exist(tools.H{"username": r.Username}) {
return nil, tools.NewValidatorError(fmt.Errorf("用户名已存在,请勿重复添加"))
}
if isql.User.Exist(tools.H{"mobile": r.Mobile}) {
return nil, tools.NewValidatorError(fmt.Errorf("手机号已存在,请勿重复添加"))
}
if isql.User.Exist(tools.H{"job_number": r.JobNumber}) {
return nil, tools.NewValidatorError(fmt.Errorf("工号已存在,请勿重复添加"))
}
if isql.User.Exist(tools.H{"mail": r.Mail}) {
return nil, tools.NewValidatorError(fmt.Errorf("邮箱已存在,请勿重复添加"))
}
// 密码通过RSA解密
// 密码不为空就解密
if r.Password != "" {
decodeData, err := tools.RSADecrypt([]byte(r.Password), config.Conf.System.RSAPrivateBytes)
if err != nil {
return nil, tools.NewValidatorError(fmt.Errorf("密码解密失败"))
}
r.Password = string(decodeData)
if len(r.Password) < 6 {
return nil, tools.NewValidatorError(fmt.Errorf("密码长度至少为6位"))
}
} else {
r.Password = "123456"
}
// 当前登陆用户角色排序最小值(最高等级角色)以及当前登陆的用户
currentRoleSortMin, ctxUser, err := isql.User.GetCurrentUserMinRoleSort(c)
if err != nil {
return nil, tools.NewValidatorError(fmt.Errorf("获取当前登陆用户角色排序最小值失败"))
}
// 根据角色id获取角色
if r.RoleIds == nil || len(r.RoleIds) == 0 {
r.RoleIds = []uint{2} // 默认添加为普通用户角色
}
roles, err := isql.Role.GetRolesByIds(r.RoleIds)
if err != nil {
return nil, tools.NewValidatorError(fmt.Errorf("根据角色ID获取角色信息失败"))
}
var reqRoleSorts []int
for _, role := range roles {
reqRoleSorts = append(reqRoleSorts, int(role.Sort))
}
// 前端传来用户角色排序最小值(最高等级角色)
reqRoleSortMin := uint(funk.MinInt(reqRoleSorts).(int))
// 当前用户的角色排序最小值 需要小于 前端传来的角色排序最小值(用户不能创建比自己等级高的或者相同等级的用户)
if currentRoleSortMin >= reqRoleSortMin {
return nil, tools.NewValidatorError(fmt.Errorf("用户不能创建比自己等级高的或者相同等级的用户"))
}
user := model.User{
Username: r.Username,
Password: r.Password,
Nickname: r.Nickname,
GivenName: r.GivenName,
Mail: r.Mail,
JobNumber: r.JobNumber,
Mobile: r.Mobile,
Avatar: r.Avatar,
PostalAddress: r.PostalAddress,
Departments: r.Departments,
Position: r.Position,
Introduction: r.Introduction,
Status: r.Status,
Creator: ctxUser.Username,
Roles: roles,
}
err = ildap.User.Add(&user)
if err != nil {
return nil, tools.NewLdapError(fmt.Errorf("向LDAP创建用户失败" + err.Error()))
}
err = isql.User.Add(&user)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("向MySQL创建用户失败"))
}
return nil, nil
}
// List 数据列表
func (l UserLogic) List(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.UserListReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
users, err := isql.User.List(r)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取用户列表失败"))
}
rets := make([]model.User, 0)
for _, user := range users {
rets = append(rets, *user)
}
count, err := isql.User.Count()
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取用户总数失败"))
}
return response.UserListRsp{
Total: int(count),
Users: rets,
}, nil
}
// Update 更新数据
func (l UserLogic) Update(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.UserUpdateReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
if !isql.User.Exist(tools.H{"id": r.ID}) {
return nil, tools.NewMySqlError(fmt.Errorf("该记录不存在"))
}
// 获取当前登陆用户
ctxUser, err := isql.User.GetCurrentLoginUser(c)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取当前登陆用户失败"))
}
// 获取当前登陆用户的所有角色
currentRoles := ctxUser.Roles
// 获取当前登陆用户角色的排序,和前端传来的角色排序做比较
var currentRoleSorts []int
// 当前登陆用户角色ID集合
var currentRoleIds []uint
for _, role := range currentRoles {
currentRoleSorts = append(currentRoleSorts, int(role.Sort))
currentRoleIds = append(currentRoleIds, role.ID)
}
// 当前登陆用户角色排序最小值(最高等级角色)
currentRoleSortMin := funk.MinInt(currentRoleSorts).(int)
// 获取前端传来的用户角色id
reqRoleIds := r.RoleIds
// 根据角色id获取角色
roles, err := isql.Role.GetRolesByIds(reqRoleIds)
if err != nil {
return nil, tools.NewValidatorError(fmt.Errorf("根据角色ID获取角色信息失败"))
}
if len(roles) == 0 {
return nil, tools.NewValidatorError(fmt.Errorf("未获取到角色信息"))
}
var reqRoleSorts []int
for _, role := range roles {
reqRoleSorts = append(reqRoleSorts, int(role.Sort))
}
// 前端传来用户角色排序最小值(最高等级角色)
reqRoleSortMin := funk.MinInt(reqRoleSorts).(int)
oldData := new(model.User)
err = isql.User.Find(tools.H{"id": r.ID}, oldData)
if err != nil {
return nil, tools.NewMySqlError(err)
}
user := model.User{
Model: oldData.Model,
Nickname: r.Nickname,
GivenName: r.GivenName,
Mail: r.Mail,
JobNumber: r.JobNumber,
Mobile: r.Mobile,
Avatar: r.Avatar,
PostalAddress: r.PostalAddress,
Departments: r.Departments,
Position: r.Position,
Introduction: r.Introduction,
Creator: ctxUser.Username,
Roles: roles,
}
// 判断是更新自己还是更新别人,如果操作的ID与登陆用户的ID一致则说明操作的是自己
if int(r.ID) == int(ctxUser.ID) {
// 不能更改自己的角色
reqDiff, currentDiff := funk.Difference(r.RoleIds, currentRoleIds)
if len(reqDiff.([]uint)) > 0 || len(currentDiff.([]uint)) > 0 {
return nil, tools.NewValidatorError(fmt.Errorf("不能更改自己的角色"))
}
} else {
// 如果是更新别人,操作者不能更新比自己角色等级高的或者相同等级的用户
// 根据userIdID获取用户角色排序最小值
minRoleSorts, err := isql.User.GetUserMinRoleSortsByIds([]uint{uint(r.ID)})
if err != nil || len(minRoleSorts) == 0 {
return nil, tools.NewValidatorError(fmt.Errorf("根据用户ID获取用户角色排序最小值失败"))
}
if currentRoleSortMin >= minRoleSorts[0] {
return nil, tools.NewValidatorError(fmt.Errorf("用户不能更新比自己角色等级高的或者相同等级的用户"))
}
// 用户不能把别的用户角色等级更新得比自己高或相等
if currentRoleSortMin >= reqRoleSortMin {
return nil, tools.NewValidatorError(fmt.Errorf("用户不能把别的用户角色等级更新得比自己高或相等"))
}
}
err = ildap.User.Update(oldData.Username, &user)
if err != nil {
return nil, tools.NewLdapError(fmt.Errorf("在LDAP更新用户失败" + err.Error()))
}
// 更新用户
err = isql.User.Update(&user)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("在MySQL更新用户失败" + err.Error()))
}
return nil, nil
}
// Delete 删除数据
func (l UserLogic) Delete(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.UserDeleteReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
// 根据用户ID获取用户角色排序最小值
roleMinSortList, err := isql.User.GetUserMinRoleSortsByIds(r.UserIds) // TODO: 这里应该复用下边的 GetUserByIds 方法
if err != nil || len(roleMinSortList) == 0 {
return nil, tools.NewValidatorError(fmt.Errorf("根据用户ID获取用户角色排序最小值失败"))
}
// 获取当前登陆用户角色排序最小值(最高等级角色)以及当前用户
minSort, ctxUser, err := isql.User.GetCurrentUserMinRoleSort(c)
if err != nil {
return nil, tools.NewValidatorError(fmt.Errorf("获取当前登陆用户角色排序最小值失败"))
}
currentRoleSortMin := int(minSort)
// 不能删除自己
if funk.Contains(r.UserIds, ctxUser.ID) {
return nil, tools.NewValidatorError(fmt.Errorf("用户不能删除自己"))
}
// 不能删除比自己(登陆用户)角色排序低(等级高)的用户
for _, sort := range roleMinSortList {
if currentRoleSortMin >= sort {
return nil, tools.NewValidatorError(fmt.Errorf("用户不能删除比自己角色等级高的用户"))
}
}
users, err := isql.User.GetUserByIds(r.UserIds)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取用户信息失败: " + err.Error()))
}
for _, user := range users {
err := ildap.User.Delete(user.Username)
if err != nil {
return nil, tools.NewLdapError(fmt.Errorf("在LDAP删除用户失败" + err.Error()))
}
}
err = isql.User.Delete(r.UserIds)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("在MySQL删除用户失败: " + err.Error()))
}
return nil, nil
}
// ChangePwd 修改密码
func (l UserLogic) ChangePwd(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.UserChangePwdReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
// 前端传来的密码是rsa加密的,先解密
// 密码通过RSA解密
decodeOldPassword, err := tools.RSADecrypt([]byte(r.OldPassword), config.Conf.System.RSAPrivateBytes)
if err != nil {
return nil, tools.NewValidatorError(fmt.Errorf("原密码解析失败"))
}
decodeNewPassword, err := tools.RSADecrypt([]byte(r.NewPassword), config.Conf.System.RSAPrivateBytes)
if err != nil {
return nil, tools.NewValidatorError(fmt.Errorf("新密码解析失败"))
}
r.OldPassword = string(decodeOldPassword)
r.NewPassword = string(decodeNewPassword)
// 获取当前用户
user, err := isql.User.GetCurrentLoginUser(c)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取当前登陆用户失败"))
}
// 获取用户的真实正确密码
// correctPasswd := user.Password
// 判断前端请求的密码是否等于真实密码
// err = tools.ComparePasswd(correctPasswd, r.OldPassword)
// if err != nil {
// return nil, tools.NewValidatorError(fmt.Errorf("原密码错误"))
// }
if tools.NewParPasswd(user.Password) != r.OldPassword {
return nil, tools.NewValidatorError(fmt.Errorf("原密码错误"))
}
// ldap更新密码时可以直接指定用户DN和新密码即可更改成功
err = ildap.User.ChangePwd(user.Username, "", r.NewPassword)
if err != nil {
return nil, tools.NewLdapError(fmt.Errorf("在LDAP更新密码失败" + err.Error()))
}
// 更新密码
err = isql.User.ChangePwd(user.Username, tools.NewGenPasswd(r.NewPassword))
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("在MySQL更新密码失败: " + err.Error()))
}
return nil, nil
}
// ChangeUserStatus 修改用户状态
func (l UserLogic) ChangeUserStatus(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.UserChangeUserStatusReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
// 校验工作
filter := tools.H{"id": r.ID}
if !isql.User.Exist(filter) {
return nil, tools.NewValidatorError(fmt.Errorf("该用户不存在"))
}
user := new(model.User)
err := isql.User.Find(filter, user)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("在MySQL查询用户失败: " + err.Error()))
}
if r.Status == 1 && r.Status == user.Status {
return nil, tools.NewValidatorError(fmt.Errorf("用户已经是在职状态"))
}
if r.Status == 2 && r.Status == user.Status {
return nil, tools.NewValidatorError(fmt.Errorf("用户已经是离职状态"))
}
// 获取当前登录用户,只有管理员才能够将用户状态改变
// 获取当前登陆用户角色排序最小值(最高等级角色)以及当前用户
minSort, _, err := isql.User.GetCurrentUserMinRoleSort(c)
if err != nil {
return nil, tools.NewValidatorError(fmt.Errorf("获取当前登陆用户角色排序最小值失败"))
}
if int(minSort) != 1 {
return nil, tools.NewValidatorError(fmt.Errorf("只有管理员才能更改用户状态"))
}
if r.Status == 2 {
err = ildap.User.Delete(user.Username)
if err != nil {
return nil, tools.NewLdapError(fmt.Errorf("在LDAP删除用户失败" + err.Error()))
}
} else {
err := ildap.User.Add(user)
if err != nil {
return nil, tools.NewLdapError(fmt.Errorf("在LDAP添加用户失败" + err.Error()))
}
}
err = isql.User.ChangeStatus(int(r.ID), int(r.Status))
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("在MySQL更新用户状态失败: " + err.Error()))
}
return nil, nil
}
// GetUserInfo 获取用户信息
func (l UserLogic) GetUserInfo(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
r, ok := req.(*request.UserGetUserInfoReq)
if !ok {
return nil, ReqAssertErr
}
_ = c
_ = r
user, err := isql.User.GetCurrentLoginUser(c)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取当前用户信息失败: " + err.Error()))
}
return user, nil
}

89
main.go Normal file
View File

@ -0,0 +1,89 @@
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/eryajf-world/go-ldap-admin/config"
"github.com/eryajf-world/go-ldap-admin/middleware"
"github.com/eryajf-world/go-ldap-admin/public/common"
"github.com/eryajf-world/go-ldap-admin/routes"
"github.com/eryajf-world/go-ldap-admin/service/isql"
)
func main() {
// 加载配置文件到全局配置结构体
config.InitConfig()
// 初始化日志
common.InitLogger()
// 初始化数据库(mysql)
common.InitMysql()
// 初始化ldap连接
common.InitLDAP()
// 初始化casbin策略管理器
common.InitCasbinEnforcer()
// 初始化Validator数据校验
common.InitValidate()
// 初始化mysql数据
common.InitData()
// 操作日志中间件处理日志时没有将日志发送到rabbitmq或者kafka中, 而是发送到了channel中
// 这里开启3个goroutine处理channel将日志记录到数据库
for i := 0; i < 3; i++ {
go isql.OperationLog.SaveOperationLogChannel(middleware.OperationLogChan)
}
// 注册所有路由
r := routes.InitRoutes()
host := "0.0.0.0"
port := config.Conf.System.Port
srv := &http.Server{
Addr: fmt.Sprintf("%s:%d", host, port),
Handler: r,
}
// Initializing the server in a goroutine so that
// it won't block the graceful shutdown handling below
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
common.Log.Fatalf("listen: %s\n", err)
}
}()
common.Log.Info(fmt.Sprintf("Server is running at %s:%d/%s", host, port, config.Conf.System.UrlPathPrefix))
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal)
// kill (no param) default send syscall.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall.SIGKILL but can't be catch, so don't need add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
common.Log.Info("Shutting down server...")
// The context is used to inform the server it has 5 seconds to finish
// the request it is currently handling
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
common.Log.Fatal("Server forced to shutdown:", err)
}
common.Log.Info("Server exiting!")
}

View File

@ -0,0 +1,140 @@
package middleware
import (
"fmt"
"github.com/eryajf-world/go-ldap-admin/config"
"github.com/eryajf-world/go-ldap-admin/model"
"github.com/eryajf-world/go-ldap-admin/public/common"
"github.com/eryajf-world/go-ldap-admin/public/tools"
"github.com/eryajf-world/go-ldap-admin/service/isql"
"time"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"github.com/eryajf-world/go-ldap-admin/svc/response"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
)
// 初始化jwt中间件
func InitAuth() (*jwt.GinJWTMiddleware, error) {
authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
Realm: config.Conf.Jwt.Realm, // jwt标识
Key: []byte(config.Conf.Jwt.Key), // 服务端密钥
Timeout: time.Hour * time.Duration(config.Conf.Jwt.Timeout), // token过期时间
MaxRefresh: time.Hour * time.Duration(config.Conf.Jwt.MaxRefresh), // token最大刷新时间(RefreshToken过期时间=Timeout+MaxRefresh)
PayloadFunc: payloadFunc, // 有效载荷处理
IdentityHandler: identityHandler, // 解析Claims
Authenticator: login, // 校验token的正确性, 处理登录逻辑
Authorizator: authorizator, // 用户登录校验成功处理
Unauthorized: unauthorized, // 用户登录校验失败处理
LoginResponse: loginResponse, // 登录成功后的响应
LogoutResponse: logoutResponse, // 登出后的响应
RefreshResponse: refreshResponse, // 刷新token后的响应
TokenLookup: "header: Authorization, query: token, cookie: jwt", // 自动在这几个地方寻找请求中的token
TokenHeadName: "Bearer", // header名称
TimeFunc: time.Now,
})
return authMiddleware, err
}
// 有效载荷处理
func payloadFunc(data interface{}) jwt.MapClaims {
if v, ok := data.(tools.H); ok {
var user model.User
// 将用户json转为结构体
tools.JsonI2Struct(v["user"], &user)
return jwt.MapClaims{
jwt.IdentityKey: user.ID,
"user": v["user"],
}
}
return jwt.MapClaims{}
}
// 解析Claims
func identityHandler(c *gin.Context) interface{} {
claims := jwt.ExtractClaims(c)
// 此处返回值类型map[string]interface{}与payloadFunc和authorizator的data类型必须一致, 否则会导致授权失败还不容易找到原因
return tools.H{
"IdentityKey": claims[jwt.IdentityKey],
"user": claims["user"],
}
}
// 校验token的正确性, 处理登录逻辑
func login(c *gin.Context) (interface{}, error) {
var req request.RegisterAndLoginReq
// 请求json绑定
if err := c.ShouldBind(&req); err != nil {
return "", err
}
// 密码通过RSA解密
decodeData, err := tools.RSADecrypt([]byte(req.Password), config.Conf.System.RSAPrivateBytes)
if err != nil {
return nil, err
}
u := &model.User{
Username: req.Username,
Password: string(decodeData),
}
// 密码校验
user, err := isql.User.Login(u)
if err != nil {
return nil, err
}
// 将用户以json格式写入, payloadFunc/authorizator会使用到
return tools.H{
"user": tools.Struct2Json(user),
}, nil
}
// 用户登录校验成功处理
func authorizator(data interface{}, c *gin.Context) bool {
if v, ok := data.(tools.H); ok {
userStr := v["user"].(string)
var user model.User
// 将用户json转为结构体
tools.Json2Struct(userStr, &user)
// 将用户保存到context, api调用时取数据方便
c.Set("user", user)
return true
}
return false
}
// 用户登录校验失败处理
func unauthorized(c *gin.Context, code int, message string) {
common.Log.Debugf("JWT认证失败, 错误码: %d, 错误信息: %s", code, message)
response.Response(c, code, code, nil, fmt.Sprintf("JWT认证失败, 错误码: %d, 错误信息: %s", code, message))
}
// 登录成功后的响应
func loginResponse(c *gin.Context, code int, token string, expires time.Time) {
response.Response(c, code, code,
gin.H{
"token": token,
"expires": expires.Format("2006-01-02 15:04:05"),
},
"登录成功")
}
// 登出后的响应
func logoutResponse(c *gin.Context, code int) {
response.Success(c, nil, "退出成功")
}
// 刷新token后的响应
func refreshResponse(c *gin.Context, code int, token string, expires time.Time) {
response.Response(c, code, code,
gin.H{
"token": token,
"expires": expires,
},
"刷新token成功")
}

View File

@ -0,0 +1,45 @@
package middleware
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
// CORS跨域中间件
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method //请求方法
origin := c.Request.Header.Get("Origin") //请求头部
var headerKeys []string // 声明请求头keys
for k, _ := range c.Request.Header {
headerKeys = append(headerKeys, k)
}
headerStr := strings.Join(headerKeys, ", ")
if headerStr != "" {
headerStr = fmt.Sprintf("access-control-allow-origin, access-control-allow-headers, %s", headerStr)
} else {
headerStr = "access-control-allow-origin, access-control-allow-headers"
}
if origin != "" {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Origin", "*") // 这是允许访问所有域
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE") //服务器支持的所有跨域请求的方法,为了避免浏览次请求的多次'预检'请求
// header的类型
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma")
// 允许跨域设置 可以返回其他子段
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar") // 跨域关键设置 让浏览器可以解析
c.Header("Access-Control-Max-Age", "172800") // 缓存请求信息 单位为秒
c.Header("Access-Control-Allow-Credentials", "false") // 跨域请求是否需要带cookie信息 默认设置为true
c.Set("content-type", "application/json") // 设置返回格式是json
}
//放行所有OPTIONS方法
if method == "OPTIONS" {
c.JSON(http.StatusOK, "Options Request!")
}
// 处理请求
c.Next() // 处理请求
}
}

View File

@ -0,0 +1,68 @@
package middleware
import (
"strings"
"sync"
"github.com/eryajf-world/go-ldap-admin/config"
"github.com/eryajf-world/go-ldap-admin/public/common"
"github.com/eryajf-world/go-ldap-admin/public/tools"
"github.com/eryajf-world/go-ldap-admin/service/isql"
"github.com/gin-gonic/gin"
)
var checkLock sync.Mutex
// Casbin中间件, 基于RBAC的权限访问控制模型
func CasbinMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
user, err := isql.User.GetCurrentLoginUser(c)
if err != nil {
tools.Response(c, 401, 401, nil, "用户未登录")
c.Abort()
return
}
if user.Status != 1 {
tools.Response(c, 401, 401, nil, "当前用户已被禁用")
c.Abort()
return
}
// 获得用户的全部角色
roles := user.Roles
// 获得用户全部未被禁用的角色的Keyword
var subs []string
for _, role := range roles {
if role.Status == 1 {
subs = append(subs, role.Keyword)
}
}
// 获得请求路径URL
obj := strings.TrimPrefix(c.FullPath(), "/"+config.Conf.System.UrlPathPrefix)
// 获取请求方式
act := c.Request.Method
isPass := check(subs, obj, act)
if !isPass {
tools.Response(c, 401, 401, nil, "没有权限")
c.Abort()
return
}
c.Next()
}
}
func check(subs []string, obj string, act string) bool {
// 同一时间只允许一个请求执行校验, 否则可能会校验失败
checkLock.Lock()
defer checkLock.Unlock()
isPass := false
for _, sub := range subs {
pass, _ := common.CasbinEnforcer.Enforce(sub, obj, act)
if pass {
isPass = true
break
}
}
return isPass
}

View File

@ -0,0 +1,70 @@
package middleware
import (
"strings"
"time"
"github.com/eryajf-world/go-ldap-admin/config"
"github.com/eryajf-world/go-ldap-admin/model"
"github.com/eryajf-world/go-ldap-admin/public/tools"
"github.com/eryajf-world/go-ldap-admin/service/isql"
"github.com/gin-gonic/gin"
)
// 操作日志channel
var OperationLogChan = make(chan *model.OperationLog, 30)
func OperationLogMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 开始时间
startTime := time.Now()
// 处理请求
c.Next()
// 结束时间
endTime := time.Now()
// 执行耗时
timeCost := endTime.Sub(startTime).Milliseconds()
// 获取当前登录用户
var username string
ctxUser, exists := c.Get("user")
if !exists {
username = "未登录"
}
user, ok := ctxUser.(model.User)
if !ok {
username = "未登录"
}
username = user.Username
// 获取访问路径
path := strings.TrimPrefix(c.FullPath(), "/"+config.Conf.System.UrlPathPrefix)
// 请求方式
method := c.Request.Method
// 获取接口描述
api := new(model.Api)
_ = isql.Api.Find(tools.H{"path": path, "method": method}, api)
operationLog := model.OperationLog{
Username: username,
Ip: c.ClientIP(),
IpLocation: "",
Method: method,
Path: path,
Remark: api.Remark,
Status: c.Writer.Status(),
StartTime: startTime,
TimeCost: timeCost,
//UserAgent: c.Request.UserAgent(),
}
// 最好是将日志发送到rabbitmq或者kafka中
// 这里是发送到channel中开启3个goroutine处理
OperationLogChan <- &operationLog
}
}

View File

@ -0,0 +1,22 @@
package middleware
import (
"time"
"github.com/eryajf-world/go-ldap-admin/svc/response"
"github.com/gin-gonic/gin"
"github.com/juju/ratelimit"
)
func RateLimitMiddleware(fillInterval time.Duration, capacity int64) gin.HandlerFunc {
bucket := ratelimit.NewBucket(fillInterval, capacity)
return func(c *gin.Context) {
if bucket.TakeAvailable(1) < 1 {
response.Fail(c, nil, "访问限流")
c.Abort()
return
}
c.Next()
}
}

12
model/api.go Normal file
View File

@ -0,0 +1,12 @@
package model
import "gorm.io/gorm"
type Api struct {
gorm.Model
Method string `gorm:"type:varchar(20);comment:'请求方式'" json:"method"`
Path string `gorm:"type:varchar(100);comment:'访问路径'" json:"path"`
Category string `gorm:"type:varchar(50);comment:'所属类别'" json:"category"`
Remark string `gorm:"type:varchar(100);comment:'备注'" json:"remark"`
Creator string `gorm:"type:varchar(20);comment:'创建人'" json:"creator"`
}

8
model/casbin.go Normal file
View File

@ -0,0 +1,8 @@
package model
// 角色权限规则
type RoleCasbin struct {
Keyword string `json:"keyword"` // 角色关键字
Path string `json:"path"` // 访问路径
Method string `json:"method"` // 请求方式
}

11
model/group.go Normal file
View File

@ -0,0 +1,11 @@
package model
import "gorm.io/gorm"
type Group struct {
gorm.Model
GroupName string `gorm:"type:varchar(20);comment:'分组名称'" json:"groupName"`
Remark string `gorm:"type:varchar(100);comment:'分组中文说明'" json:"remark"`
Creator string `gorm:"type:varchar(20);comment:'创建人'" json:"creator"`
Users []*User `gorm:"many2many:group_users" json:"users"`
}

26
model/menu.go Normal file
View File

@ -0,0 +1,26 @@
package model
import (
"gorm.io/gorm"
)
type Menu struct {
gorm.Model
Name string `gorm:"type:varchar(50);comment:'菜单名称(英文名, 可用于国际化)'" json:"name"`
Title string `gorm:"type:varchar(50);comment:'菜单标题(无法国际化时使用)'" json:"title"`
Icon string `gorm:"type:varchar(50);comment:'菜单图标'" json:"icon"`
Path string `gorm:"type:varchar(100);comment:'菜单访问路径'" json:"path"`
Redirect string `gorm:"type:varchar(100);comment:'重定向路径'" json:"redirect"`
Component string `gorm:"type:varchar(100);comment:'前端组件路径'" json:"component"`
Sort uint `gorm:"type:int(3) unsigned;default:999;comment:'菜单顺序(1-999)'" json:"sort"`
Status uint `gorm:"type:tinyint(1);default:1;comment:'菜单状态(正常/禁用, 默认正常)'" json:"status"`
Hidden uint `gorm:"type:tinyint(1);default:2;comment:'菜单在侧边栏隐藏(1隐藏2显示)'" json:"hidden"`
NoCache uint `gorm:"type:tinyint(1);default:2;comment:'菜单是否被 <keep-alive> 缓存(1不缓存2缓存)'" json:"noCache"`
AlwaysShow uint `gorm:"type:tinyint(1);default:2;comment:'忽略之前定义的规则,一直显示根路由(1忽略2不忽略)'" json:"alwaysShow"`
Breadcrumb uint `gorm:"type:tinyint(1);default:1;comment:'面包屑可见性(可见/隐藏, 默认可见)'" json:"breadcrumb"`
ActiveMenu string `gorm:"type:varchar(100);comment:'在其它路由时,想在侧边栏高亮的路由'" json:"activeMenu"`
ParentId uint `gorm:"default:0;comment:'父菜单编号(编号为0时表示根菜单)'" json:"parentId"`
Creator string `gorm:"type:varchar(20);comment:'创建人'" json:"creator"`
Children []*Menu `gorm:"-" json:"children"` // 子菜单集合
Roles []*Role `gorm:"many2many:role_menus;" json:"roles"` // 角色菜单多对多关系
}

21
model/operation_log.go Normal file
View File

@ -0,0 +1,21 @@
package model
import (
"time"
"gorm.io/gorm"
)
type OperationLog struct {
gorm.Model
Username string `gorm:"type:varchar(20);comment:'用户登录名'" json:"username"`
Ip string `gorm:"type:varchar(20);comment:'Ip地址'" json:"ip"`
IpLocation string `gorm:"type:varchar(20);comment:'Ip所在地'" json:"ipLocation"`
Method string `gorm:"type:varchar(20);comment:'请求方式'" json:"method"`
Path string `gorm:"type:varchar(100);comment:'访问路径'" json:"path"`
Remark string `gorm:"type:varchar(100);comment:'备注'" json:"remark"`
Status int `gorm:"type:int(4);comment:'响应状态码'" json:"status"`
StartTime time.Time `gorm:"type:datetime(3);comment:'发起时间'" json:"startTime"`
TimeCost int64 `gorm:"type:int(6);comment:'请求耗时(ms)'" json:"timeCost"`
UserAgent string `gorm:"type:varchar(20);comment:'浏览器标识'" json:"userAgent"`
}

15
model/role.go Normal file
View File

@ -0,0 +1,15 @@
package model
import "gorm.io/gorm"
type Role struct {
gorm.Model
Name string `gorm:"type:varchar(20);not null;unique" json:"name"`
Keyword string `gorm:"type:varchar(20);not null;unique" json:"keyword"`
Remark string `gorm:"type:varchar(100);comment:'备注'" json:"remark"`
Status uint `gorm:"type:tinyint(1);default:1;comment:'1正常, 2禁用'" json:"status"`
Sort uint `gorm:"type:int(3);default:999;comment:'角色排序(排序越大权限越低, 不能查看比自己序号小的角色, 不能编辑同序号用户权限, 排序为1表示超级管理员)'" json:"sort"`
Creator string `gorm:"type:varchar(20);" json:"creator"`
Users []*User `gorm:"many2many:user_roles" json:"users"`
Menus []*Menu `gorm:"many2many:role_menus;" json:"menus"` // 角色菜单多对多关系
}

22
model/user.go Normal file
View File

@ -0,0 +1,22 @@
package model
import "gorm.io/gorm"
type User struct {
gorm.Model
Username string `gorm:"type:varchar(10);not null;unique" json:"username"` // 用户名
Password string `gorm:"size:255;not null" json:"password"` // 用户密码
Nickname string `gorm:"type:varchar(10)" json:"nickname"` // 昵称
GivenName string `gorm:"type:varchar(10)" json:"givenName"` // 花名,如果有的话,没有的话用昵称占位
Mail string `gorm:"type:varchar(20)" json:"mail"` // 邮箱
JobNumber string `gorm:"type:varchar(5)" json:"jobNumber"` // 工号
Mobile string `gorm:"type:varchar(11);not null;unique" json:"mobile"` // 手机号
Avatar string `gorm:"type:varchar(255)" json:"avatar"` // 头像
PostalAddress string `gorm:"type:varchar(255)" json:"postalAddress"` // 地址
Departments string `gorm:"type:varchar(128)" json:"departments"` // 部门
Position string `gorm:"type:varchar(128)" json:"position"` // 职位
Introduction string `gorm:"type:varchar(255)" json:"introduction"` // 个人简介
Status uint `gorm:"type:tinyint(1);default:1;comment:'1在职, 2离职'" json:"status"` // 状态
Creator string `gorm:"type:varchar(20);" json:"creator"` // 创建者
Roles []*Role `gorm:"many2many:user_roles" json:"roles"` // 角色
}

42
public/common/casbin.go Normal file
View File

@ -0,0 +1,42 @@
package common
import (
"fmt"
"github.com/eryajf-world/go-ldap-admin/config"
"github.com/casbin/casbin/v2"
gormadapter "github.com/casbin/gorm-adapter/v3"
)
// 全局CasbinEnforcer
var CasbinEnforcer *casbin.Enforcer
// 初始化casbin策略管理器
func InitCasbinEnforcer() {
e, err := mysqlCasbin()
if err != nil {
Log.Panicf("初始化Casbin失败%v", err)
panic(fmt.Sprintf("初始化Casbin失败%v", err))
}
CasbinEnforcer = e
Log.Info("初始化Casbin完成!")
}
func mysqlCasbin() (*casbin.Enforcer, error) {
a, err := gormadapter.NewAdapterByDB(DB)
if err != nil {
return nil, err
}
e, err := casbin.NewEnforcer(config.Conf.Casbin.ModelPath, a)
if err != nil {
return nil, err
}
err = e.LoadPolicy()
if err != nil {
return nil, err
}
return e, nil
}

74
public/common/database.go Normal file
View File

@ -0,0 +1,74 @@
package common
import (
"fmt"
"github.com/eryajf-world/go-ldap-admin/config"
"github.com/eryajf-world/go-ldap-admin/model"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// 全局mysql数据库变量
var DB *gorm.DB
// 初始化mysql数据库
func InitMysql() {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&collation=%s&%s",
config.Conf.Mysql.Username,
config.Conf.Mysql.Password,
config.Conf.Mysql.Host,
config.Conf.Mysql.Port,
config.Conf.Mysql.Database,
config.Conf.Mysql.Charset,
config.Conf.Mysql.Collation,
config.Conf.Mysql.Query,
)
// 隐藏密码
showDsn := fmt.Sprintf(
"%s:******@tcp(%s:%d)/%s?charset=%s&collation=%s&%s",
config.Conf.Mysql.Username,
config.Conf.Mysql.Host,
config.Conf.Mysql.Port,
config.Conf.Mysql.Database,
config.Conf.Mysql.Charset,
config.Conf.Mysql.Collation,
config.Conf.Mysql.Query,
)
// Log.Info("数据库连接DSN: ", showDsn)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
// 禁用外键(指定外键时不会在mysql创建真实的外键约束)
DisableForeignKeyConstraintWhenMigrating: true,
//// 指定表前缀
//NamingStrategy: schema.NamingStrategy{
// TablePrefix: config.Conf.Mysql.TablePrefix + "_",
//},
})
if err != nil {
Log.Panicf("初始化mysql数据库异常: %v", err)
panic(fmt.Errorf("初始化mysql数据库异常: %v", err))
}
// 开启mysql日志
if config.Conf.Mysql.LogMode {
db.Debug()
}
// 全局DB赋值
DB = db
// 自动迁移表结构
dbAutoMigrate()
Log.Infof("初始化mysql数据库完成! dsn: %s", showDsn)
}
// 自动迁移表结构
func dbAutoMigrate() {
DB.AutoMigrate(
&model.User{},
&model.Role{},
&model.Group{},
&model.Menu{},
&model.Api{},
&model.OperationLog{},
)
}

View File

@ -0,0 +1,592 @@
package common
import (
"errors"
"github.com/eryajf-world/go-ldap-admin/config"
"github.com/eryajf-world/go-ldap-admin/model"
"github.com/eryajf-world/go-ldap-admin/public/tools"
"github.com/thoas/go-funk"
"gorm.io/gorm"
)
// 初始化mysql数据
func InitData() {
// 是否初始化数据
if !config.Conf.System.InitData {
return
}
// 1.写入角色数据
newRoles := make([]*model.Role, 0)
roles := []*model.Role{
{
Model: gorm.Model{ID: 1},
Name: "管理员",
Keyword: "admin",
Remark: "",
Sort: 1,
Status: 1,
Creator: "系统",
},
{
Model: gorm.Model{ID: 2},
Name: "普通用户",
Keyword: "user",
Remark: "",
Sort: 3,
Status: 1,
Creator: "系统",
},
{
Model: gorm.Model{ID: 3},
Name: "访客",
Keyword: "guest",
Remark: "",
Sort: 5,
Status: 1,
Creator: "系统",
},
}
for _, role := range roles {
err := DB.First(&role, role.ID).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
newRoles = append(newRoles, role)
}
}
if len(newRoles) > 0 {
err := DB.Create(&newRoles).Error
if err != nil {
Log.Errorf("写入系统角色数据失败:%v", err)
}
}
// 2写入菜单
newMenus := make([]model.Menu, 0)
var uint0 uint = 0
var uint1 uint = 1
var uint4 uint = 4
var uint8 uint = 8
componentStr := "component"
systemRoleStr := "/system/role"
personnelManageStr := "/personnel/user"
userStr := "user"
peopleStr := "people"
groupStr := "peoples"
roleStr := "eye-open"
treeTableStr := "tree-table"
treeStr := "tree"
exampleStr := "example"
logOperationStr := "/log/operation-log"
documentationStr := "documentation"
menus := []model.Menu{
{
Model: gorm.Model{ID: 1},
Name: "UserManage",
Title: "人员管理",
Icon: userStr,
Path: "/personnel",
Component: "Layout",
Redirect: personnelManageStr,
Sort: 6,
ParentId: uint0,
Roles: roles[:1],
Creator: "系统",
},
{
Model: gorm.Model{ID: 2},
Name: "User",
Title: "用户管理",
Icon: peopleStr,
Path: "user",
Component: "/personnel/user/index",
Sort: 7,
ParentId: uint1,
Roles: roles[:1],
Creator: "系统",
},
{
Model: gorm.Model{ID: 3},
Name: "Group",
Title: "分组管理",
Icon: groupStr,
Path: "group",
Component: "/personnel/group/index",
Sort: 8,
ParentId: uint1,
Roles: roles[:1],
Creator: "系统",
},
{
Model: gorm.Model{ID: 4},
Name: "System",
Title: "系统管理",
Icon: componentStr,
Path: "/system",
Component: "Layout",
Redirect: systemRoleStr,
Sort: 9,
ParentId: uint0,
Roles: roles[:1],
Creator: "系统",
},
{
Model: gorm.Model{ID: 5},
Name: "Role",
Title: "角色管理",
Icon: roleStr,
Path: "role",
Component: "/system/role/index",
Sort: 10,
ParentId: uint4,
Roles: roles[:1],
Creator: "系统",
},
{
Model: gorm.Model{ID: 6},
Name: "Menu",
Title: "菜单管理",
Icon: treeTableStr,
Path: "menu",
Component: "/system/menu/index",
Sort: 13,
ParentId: uint4,
Roles: roles[:1],
Creator: "系统",
},
{
Model: gorm.Model{ID: 7},
Name: "Api",
Title: "接口管理",
Icon: treeStr,
Path: "api",
Component: "/system/api/index",
Sort: 14,
ParentId: uint4,
Roles: roles[:1],
Creator: "系统",
},
{
Model: gorm.Model{ID: 8},
Name: "Log",
Title: "日志管理",
Icon: exampleStr,
Path: "/log",
Component: "Layout",
Redirect: logOperationStr,
Sort: 20,
ParentId: uint0,
Roles: roles[:2],
Creator: "系统",
},
{
Model: gorm.Model{ID: 9},
Name: "OperationLog",
Title: "操作日志",
Icon: documentationStr,
Path: "operation-log",
Component: "/log/operation-log/index",
Sort: 21,
ParentId: uint8,
Roles: roles[:2],
Creator: "系统",
},
}
for _, menu := range menus {
err := DB.First(&menu, menu.ID).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
newMenus = append(newMenus, menu)
}
}
if len(newMenus) > 0 {
err := DB.Create(&newMenus).Error
if err != nil {
Log.Errorf("写入系统菜单数据失败:%v", err)
}
}
// 3.写入用户
newUsers := make([]model.User, 0)
users := []model.User{
{
Model: gorm.Model{ID: 1},
Username: "admin",
Password: tools.NewGenPasswd(config.Conf.Ldap.LdapAdminPass),
Nickname: "管理员",
GivenName: "最强后台",
Mail: "admin@eryajf.net",
JobNumber: "0000",
Mobile: "18888888888",
Avatar: "https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif",
PostalAddress: "中国河南省南阳市",
Departments: "运维部",
Position: "系统管理员",
Introduction: "最强后台的管理员",
Status: 1,
Creator: "系统",
Roles: roles[:1],
},
}
for _, user := range users {
err := DB.First(&user, user.ID).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
newUsers = append(newUsers, user)
}
}
if len(newUsers) > 0 {
err := DB.Create(&newUsers).Error
if err != nil {
Log.Errorf("写入用户数据失败:%v", err)
}
}
// 4.写入api
apis := []model.Api{
{
Method: "POST",
Path: "/base/login",
Category: "base",
Remark: "用户登录",
Creator: "系统",
},
{
Method: "POST",
Path: "/base/logout",
Category: "base",
Remark: "用户登出",
Creator: "系统",
},
{
Method: "POST",
Path: "/base/refreshToken",
Category: "base",
Remark: "刷新JWT令牌",
Creator: "系统",
},
{
Method: "POST",
Path: "/base/changePwd",
Category: "base",
Remark: "通过邮箱修改密码",
Creator: "系统",
},
{
Method: "GET",
Path: "/user/info",
Category: "user",
Remark: "获取当前登录用户信息",
Creator: "系统",
},
{
Method: "GET",
Path: "/user/list",
Category: "user",
Remark: "获取用户列表",
Creator: "系统",
},
{
Method: "POST",
Path: "/user/changePwd",
Category: "user",
Remark: "更新用户登录密码",
Creator: "系统",
},
{
Method: "POST",
Path: "/user/add",
Category: "user",
Remark: "创建用户",
Creator: "系统",
},
{
Method: "POST",
Path: "/user/update",
Category: "user",
Remark: "更新用户",
Creator: "系统",
},
{
Method: "POST",
Path: "/user/delete",
Category: "user",
Remark: "批量删除用户",
Creator: "系统",
},
{
Method: "POST",
Path: "/user/changeUserStatus",
Category: "user",
Remark: "更改用户在职状态",
Creator: "系统",
},
{
Method: "GET",
Path: "/group/list",
Category: "group",
Remark: "获取分组列表",
Creator: "系统",
},
{
Method: "POST",
Path: "/group/add",
Category: "group",
Remark: "创建分组",
Creator: "系统",
},
{
Method: "POST",
Path: "/group/update",
Category: "group",
Remark: "更新分组",
Creator: "系统",
},
{
Method: "POST",
Path: "/group/delete",
Category: "group",
Remark: "批量删除分组",
Creator: "系统",
},
{
Method: "POST",
Path: "/group/adduser",
Category: "group",
Remark: "添加用户到分组",
Creator: "系统",
},
{
Method: "POST",
Path: "/group/removeuser",
Category: "group",
Remark: "将用户从分组移出",
Creator: "系统",
},
{
Method: "GET",
Path: "/group/useringroup",
Category: "group",
Remark: "获取在分组内的用户列表",
Creator: "系统",
},
{
Method: "GET",
Path: "/group/usernoingroup",
Category: "group",
Remark: "获取不在分组内的用户列表",
Creator: "系统",
},
{
Method: "GET",
Path: "/role/list",
Category: "role",
Remark: "获取角色列表",
Creator: "系统",
},
{
Method: "POST",
Path: "/role/add",
Category: "role",
Remark: "创建角色",
Creator: "系统",
},
{
Method: "POST",
Path: "/role/update",
Category: "role",
Remark: "更新角色",
Creator: "系统",
},
{
Method: "GET",
Path: "/role/getmenulist",
Category: "role",
Remark: "获取角色的权限菜单",
Creator: "系统",
},
{
Method: "POST",
Path: "/role/updatemenus",
Category: "role",
Remark: "更新角色的权限菜单",
Creator: "系统",
},
{
Method: "GET",
Path: "/role/getapilist",
Category: "role",
Remark: "获取角色的权限接口",
Creator: "系统",
},
{
Method: "POST",
Path: "/role/updateapis",
Category: "role",
Remark: "更新角色的权限接口",
Creator: "系统",
},
{
Method: "POST",
Path: "/role/delete",
Category: "role",
Remark: "批量删除角色",
Creator: "系统",
},
{
Method: "GET",
Path: "/menu/list",
Category: "menu",
Remark: "获取菜单列表",
Creator: "系统",
},
{
Method: "GET",
Path: "/menu/tree",
Category: "menu",
Remark: "获取菜单树",
Creator: "系统",
},
{
Method: "POST",
Path: "/menu/add",
Category: "menu",
Remark: "创建菜单",
Creator: "系统",
},
{
Method: "POST",
Path: "/menu/update",
Category: "menu",
Remark: "更新菜单",
Creator: "系统",
},
{
Method: "POST",
Path: "/menu/delete",
Category: "menu",
Remark: "批量删除菜单",
Creator: "系统",
},
{
Method: "GET",
Path: "/api/list",
Category: "api",
Remark: "获取接口列表",
Creator: "系统",
},
{
Method: "GET",
Path: "/api/tree",
Category: "api",
Remark: "获取接口树",
Creator: "系统",
},
{
Method: "POST",
Path: "/api/add",
Category: "api",
Remark: "创建接口",
Creator: "系统",
},
{
Method: "POST",
Path: "/api/update",
Category: "api",
Remark: "更新接口",
Creator: "系统",
},
{
Method: "POST",
Path: "/api/delete",
Category: "api",
Remark: "批量删除接口",
Creator: "系统",
},
{
Method: "GET",
Path: "/log/operation/list",
Category: "log",
Remark: "获取操作日志列表",
Creator: "系统",
},
{
Method: "POST",
Path: "/log/operation/delete",
Category: "log",
Remark: "批量删除操作日志",
Creator: "系统",
},
}
// 5. 将角色绑定给菜单
newApi := make([]model.Api, 0)
newRoleCasbin := make([]model.RoleCasbin, 0)
for i, api := range apis {
api.ID = uint(i + 1)
err := DB.First(&api, api.ID).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
newApi = append(newApi, api)
// 管理员拥有所有API权限
newRoleCasbin = append(newRoleCasbin, model.RoleCasbin{
Keyword: roles[0].Keyword,
Path: api.Path,
Method: api.Method,
})
// 非管理员拥有基础权限
basePaths := []string{
"/base/login",
"/base/logout",
"/base/refreshToken",
"/base/changePwd",
"/base/dashboard",
"/user/info",
"/user/list",
"/user/changePwd",
"/group/list",
"/group/useringroup",
"/group/usernoingroup",
"/role/list",
"/role/getmenulist",
"/role/getapilist",
"/menu/tree",
"/menu/list",
"/api/list",
"/api/tree",
"/log/operation/list",
}
if funk.ContainsString(basePaths, api.Path) {
newRoleCasbin = append(newRoleCasbin, model.RoleCasbin{
Keyword: roles[1].Keyword,
Path: api.Path,
Method: api.Method,
})
}
}
}
if len(newApi) > 0 {
if err := DB.Create(&newApi).Error; err != nil {
Log.Errorf("写入api数据失败%v", err)
}
}
if len(newRoleCasbin) > 0 {
rules := make([][]string, 0)
for _, c := range newRoleCasbin {
rules = append(rules, []string{
c.Keyword, c.Path, c.Method,
})
}
isAdd, err := CasbinEnforcer.AddPolicies(rules)
if !isAdd {
Log.Errorf("写入casbin数据失败%v", err)
}
}
}

41
public/common/ldap.go Normal file
View File

@ -0,0 +1,41 @@
package common
import (
"fmt"
"net"
"time"
"github.com/eryajf-world/go-ldap-admin/config"
ldap "github.com/go-ldap/ldap/v3"
)
// 全局mysql数据库变量
var LDAP *ldap.Conn
// Init 初始化连接
func InitLDAP() {
// Dail有两个参数 network, address, 返回 (*Conn, error)
ldap, err := ldap.DialURL(config.Conf.Ldap.LdapUrl, ldap.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second}))
if err != nil {
Log.Panicf("初始化ldap连接异常: %v", err)
panic(fmt.Errorf("初始化ldap连接异常: %v", err))
}
err = ldap.Bind(config.Conf.Ldap.LdapAdminDN, config.Conf.Ldap.LdapAdminPass)
if err != nil {
Log.Panicf("绑定admin账号异常: %v", err)
panic(fmt.Errorf("绑定admin账号异常: %v", err))
}
// 全局LDAP赋值
LDAP = ldap
// 隐藏密码
showDsn := fmt.Sprintf(
"%s:******@tcp(%s)",
config.Conf.Ldap.LdapAdminDN,
config.Conf.Ldap.LdapUrl,
)
Log.Info("初始化ldap完成! dsn: ", showDsn)
}

111
public/common/logger.go Normal file
View File

@ -0,0 +1,111 @@
package common
import (
"fmt"
"os"
"time"
"github.com/eryajf-world/go-ldap-admin/config"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
// 全局日志变量
//var Log *zap.Logger
var Log *zap.SugaredLogger
/**
* 初始化日志
* filename 日志文件路径
* level 日志级别
* maxSize 每个日志文件保存的最大尺寸 单位M
* maxBackups 日志文件最多保存多少个备份
* maxAge 文件最多保存多少天
* compress 是否压缩
* serviceName 服务名
* 由于zap不具备日志切割功能, 这里使用lumberjack配合
*/
func InitLogger() {
now := time.Now()
infoLogFileName := fmt.Sprintf("%s/info/%04d-%02d-%02d.log", config.Conf.Logs.Path, now.Year(), now.Month(), now.Day())
errorLogFileName := fmt.Sprintf("%s/error/%04d-%02d-%02d.log", config.Conf.Logs.Path, now.Year(), now.Month(), now.Day())
var coreArr []zapcore.Core
// 获取编码器
//encoderConfig := zap.NewProductionEncoderConfig()
//encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder // 指定时间格式
//encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder // 不需要的话取值zapcore.CapitalLevelEncoder就可以了
////encoderConfig.EncodeCaller = zapcore.FullCallerEncoder // 显示完整文件路径
encoderConfig := zapcore.EncoderConfig{
MessageKey: "msg",
LevelKey: "level",
TimeKey: "time",
NameKey: "name",
CallerKey: "file",
FunctionKey: "func",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalLevelEncoder,
EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format("2006-01-02 15:04:05"))
},
//EncodeTime: zapcore.ISO8601TimeEncoder, // ISO8601 UTC 时间格式
//EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) {
// enc.AppendInt64(int64(d) / 1000000)
//},
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
//EncodeCaller: zapcore.FullCallerEncoder,
//EncodeName: nil,
//ConsoleSeparator: "",
}
encoder := zapcore.NewConsoleEncoder(encoderConfig)
// 日志级别
highPriority := zap.LevelEnablerFunc(func(level zapcore.Level) bool {
return level >= zap.ErrorLevel
})
lowPriority := zap.LevelEnablerFunc(func(level zapcore.Level) bool {
return level < zap.ErrorLevel && level >= zap.DebugLevel
})
// 当yml配置中的等级大于Error时lowPriority级别日志停止记录
if config.Conf.Logs.Level >= 2 {
lowPriority = zap.LevelEnablerFunc(func(level zapcore.Level) bool {
return false
})
}
// info文件writeSyncer
infoFileWriteSyncer := zapcore.AddSync(&lumberjack.Logger{
Filename: infoLogFileName, //日志文件存放目录,如果文件夹不存在会自动创建
MaxSize: config.Conf.Logs.MaxSize, //文件大小限制,单位MB
MaxAge: config.Conf.Logs.MaxAge, //日志文件保留天数
MaxBackups: config.Conf.Logs.MaxBackups, //最大保留日志文件数量
LocalTime: false,
Compress: config.Conf.Logs.Compress, //是否压缩处理
})
// 第三个及之后的参数为写入文件的日志级别,ErrorLevel模式只记录error级别的日志
infoFileCore := zapcore.NewCore(encoder, zapcore.NewMultiWriteSyncer(infoFileWriteSyncer, zapcore.AddSync(os.Stdout)), lowPriority)
// error文件writeSyncer
errorFileWriteSyncer := zapcore.AddSync(&lumberjack.Logger{
Filename: errorLogFileName, //日志文件存放目录
MaxSize: config.Conf.Logs.MaxSize, //文件大小限制,单位MB
MaxAge: config.Conf.Logs.MaxAge, //日志文件保留天数
MaxBackups: config.Conf.Logs.MaxBackups, //最大保留日志文件数量
LocalTime: false,
Compress: config.Conf.Logs.Compress, //是否压缩处理
})
// 第三个及之后的参数为写入文件的日志级别,ErrorLevel模式只记录error级别的日志
errorFileCore := zapcore.NewCore(encoder, zapcore.NewMultiWriteSyncer(errorFileWriteSyncer, zapcore.AddSync(os.Stdout)), highPriority)
coreArr = append(coreArr, infoFileCore)
coreArr = append(coreArr, errorFileCore)
logger := zap.New(zapcore.NewTee(coreArr...), zap.AddCaller())
Log = logger.Sugar()
Log.Info("初始化zap日志完成!")
}

View File

@ -0,0 +1,34 @@
package common
import (
"regexp"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
ch_translations "github.com/go-playground/validator/v10/translations/zh"
)
// 全局Validate数据校验实列
var Validate *validator.Validate
// 全局翻译器
var Trans ut.Translator
// 初始化Validator数据校验
func InitValidate() {
chinese := zh.New()
uni := ut.New(chinese, chinese)
trans, _ := uni.GetTranslator("zh")
Trans = trans
Validate = validator.New()
_ = ch_translations.RegisterDefaultTranslations(Validate, Trans)
_ = Validate.RegisterValidation("checkMobile", checkMobile)
Log.Infof("初始化validator.v10数据校验器完成")
}
func checkMobile(fl validator.FieldLevel) bool {
reg := `^1([38][0-9]|14[579]|5[^4]|16[6]|7[1-35-8]|9[189])\d{8}$`
rgx := regexp.MustCompile(reg)
return rgx.MatchString(fl.Field().String())
}

View File

@ -0,0 +1,34 @@
package tools
import (
"github.com/eryajf-world/go-ldap-admin/config"
)
// 密码加密 使用自适应hash算法, 不可逆
// func GenPasswd(passwd string) string {
// hashPasswd, _ := bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.DefaultCost)
// return string(hashPasswd)
// }
// 通过比较两个字符串hash判断是否出自同一个明文
// hashPasswd 需要对比的密文
// passwd 明文
// func ComparePasswd(hashPasswd string, passwd string) error {
// // if err := bcrypt.CompareHashAndPassword([]byte(hashPasswd), []byte(passwd)); err != nil {
// // return err
// // }
// return nil
// }
// 密码加密
func NewGenPasswd(passwd string) string {
pass, _ := RSAEncrypt([]byte(passwd), config.Conf.System.RSAPublicBytes)
return string(pass)
}
// 密码解密
func NewParPasswd(passwd string) string {
pass, _ := RSADecrypt([]byte(passwd), config.Conf.System.RSAPrivateBytes)
return string(pass)
}

38
public/tools/email.go Normal file
View File

@ -0,0 +1,38 @@
package tools
import (
"fmt"
"github.com/eryajf-world/go-ldap-admin/config"
"strconv"
"gopkg.in/gomail.v2"
)
func email(mailTo []string, subject string, body string) error {
mailConn := map[string]string{
"user": config.Conf.Email.User,
"pass": config.Conf.Email.Pass,
"host": config.Conf.Email.Host,
"port": config.Conf.Email.Port,
}
port, _ := strconv.Atoi(mailConn["port"]) //转换端口类型为int
newmail := gomail.NewMessage()
newmail.SetHeader("From", newmail.FormatAddress(mailConn["user"], config.Conf.Email.From))
newmail.SetHeader("To", mailTo...) //发送给多个用户
newmail.SetHeader("Subject", subject) //设置邮件主题
newmail.SetBody("text/html", body) //设置邮件正文
do := gomail.NewDialer(mailConn["host"], port, mailConn["user"], mailConn["pass"])
return do.DialAndSend(newmail)
}
func SendMail(sendto []string, pass string) error {
subject := "重置LDAP密码成功"
// 邮件正文
body := fmt.Sprintf("<li><a>更改之后的密码为:%s</a></li>", pass)
return email(sendto, subject, body)
}

103
public/tools/http.go Normal file
View File

@ -0,0 +1,103 @@
package tools
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
const (
SystemErr = 500
MySqlErr = 501
LdapErr = 505
OperationErr = 506
ValidatorErr = 412
)
type RspError struct {
code int
err error
}
func (re *RspError) Error() string {
return re.err.Error()
}
func (re *RspError) Code() int {
return re.code
}
// NewRspError New
func NewRspError(code int, err error) *RspError {
return &RspError{
code: code,
err: err,
}
}
// NewMySqlError mysql错误
func NewMySqlError(err error) *RspError {
return NewRspError(MySqlErr, err)
}
// NewValidatorError 验证错误
func NewValidatorError(err error) *RspError {
return NewRspError(ValidatorErr, err)
}
// NewLdapError ldap错误
func NewLdapError(err error) *RspError {
return NewRspError(LdapErr, err)
}
// NewOperationError 操作错误
func NewOperationError(err error) *RspError {
return NewRspError(OperationErr, err)
}
// ReloadErr 重新加载错误
func ReloadErr(err interface{}) *RspError {
rspErr, ok := err.(*RspError)
if !ok {
rspError, ok := err.(error)
if !ok {
return &RspError{
code: SystemErr,
err: fmt.Errorf("unknow error"),
}
}
return &RspError{
code: SystemErr,
err: rspError,
}
}
return rspErr
}
// Success http 成功
func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "success",
"data": data,
})
}
// Err http 错误
func Err(c *gin.Context, err *RspError, data interface{}) {
c.JSON(http.StatusOK, gin.H{
"code": err.Code(),
"msg": err.Error(),
"data": data,
})
}
// 返回前端
func Response(c *gin.Context, httpStatus int, code int, data gin.H, message string) {
c.JSON(httpStatus, gin.H{
"code": code,
"data": data,
"message": message,
})
}

30
public/tools/json.go Normal file
View File

@ -0,0 +1,30 @@
package tools
import (
"encoding/json"
"fmt"
)
// 结构体转为json
func Struct2Json(obj interface{}) string {
str, err := json.Marshal(obj)
if err != nil {
panic(fmt.Sprintf("[Struct2Json]转换异常: %v", err))
}
return string(str)
}
// json转为结构体
func Json2Struct(str string, obj interface{}) {
// 将json转为结构体
err := json.Unmarshal([]byte(str), obj)
if err != nil {
panic(fmt.Sprintf("[Json2Struct]转换异常: %v", err))
}
}
// json interface转为结构体
func JsonI2Struct(str interface{}, obj interface{}) {
JsonStr := str.(string)
Json2Struct(JsonStr, obj)
}

69
public/tools/rsa.go Normal file
View File

@ -0,0 +1,69 @@
package tools
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
)
// RSA加密
func RSAEncrypt(data, publicBytes []byte) ([]byte, error) {
var res []byte
// 解析公钥
block, _ := pem.Decode(publicBytes)
if block == nil {
return res, fmt.Errorf("无法加密, 公钥可能不正确")
}
// 使用X509将解码之后的数据 解析出来
// x509.MarshalPKCS1PublicKey(block):解析之后无法用所以采用以下方法ParsePKIXPublicKey
keyInit, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return res, fmt.Errorf("无法加密, 公钥可能不正确, %v", err)
}
// 使用公钥加密数据
pubKey := keyInit.(*rsa.PublicKey)
res, err = rsa.EncryptPKCS1v15(rand.Reader, pubKey, data)
if err != nil {
return res, fmt.Errorf("无法加密, 公钥可能不正确, %v", err)
}
// 将数据加密为base64格式
return []byte(EncodeStr2Base64(string(res))), nil
}
// 对数据进行解密操作
func RSADecrypt(base64Data, privateBytes []byte) ([]byte, error) {
var res []byte
// 将base64数据解析
data := []byte(DecodeStrFromBase64(string(base64Data)))
// 解析私钥
block, _ := pem.Decode(privateBytes)
if block == nil {
return res, fmt.Errorf("无法解密, 私钥可能不正确,解析私钥失败")
}
// 还原数据
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return res, fmt.Errorf("无法解密, 私钥可能不正确,解析PKCS失败 %v", err)
}
res, err = rsa.DecryptPKCS1v15(rand.Reader, privateKey, data)
if err != nil {
return res, fmt.Errorf("无法解密, 私钥可能不正确,解密PKCS1v15失败 %v", err)
}
return res, nil
}
// 加密base64字符串
func EncodeStr2Base64(str string) string {
return base64.StdEncoding.EncodeToString([]byte(str))
}
// 解密base64字符串
func DecodeStrFromBase64(str string) string {
decodeBytes, _ := base64.StdEncoding.DecodeString(str)
return string(decodeBytes)
}

4
public/tools/type.go Normal file
View File

@ -0,0 +1,4 @@
package tools
// H is a shortcut for map[string]interface{}
type H map[string]interface{}

14
public/tools/util_test.go Normal file
View File

@ -0,0 +1,14 @@
package tools
import (
"fmt"
"testing"
)
func TestGenPass(t *testing.T) {
fmt.Printf("密码为:%s\n", NewGenPasswd("123456"))
// err := ComparePasswd("$2a$10$Fy8p0nCixgWKzLfO3SgdhOzAF7YolSt6dHj6QidDGYlzLJDpniXB6", "123456")
// if err != nil {
// fmt.Printf("密码错误:%s\n", err)
// }
}

29
public/tools/web.go Normal file
View File

@ -0,0 +1,29 @@
package tools
type PageOption struct {
PageNum int `json:"pageNum"`
PageSize int `json:"pageSize"`
}
var defaultOptions *PageOption
func init() {
// 默认取 第 1 页的 10 条记录
defaultOptions = &PageOption{
PageNum: 0,
PageSize: 10,
}
}
// NewPageOption 创建一个分页参数
func NewPageOption(pageNum, pageSize int) *PageOption {
if !(pageSize > 0 && pageSize < 1000) || pageNum < 0 || pageSize <= 0 {
return defaultOptions
}
pNum := (pageNum - 1) * pageSize
return &PageOption{
PageNum: pNum,
PageSize: pageSize,
}
}

14
rbac_model.conf Normal file
View File

@ -0,0 +1,14 @@
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && (keyMatch2(r.obj, p.obj) || keyMatch(r.obj, p.obj)) && (r.act == p.act || p.act == "*")

59
routes/a_routes.go Normal file
View File

@ -0,0 +1,59 @@
package routes
import (
"fmt"
"time"
"github.com/eryajf-world/go-ldap-admin/config"
"github.com/eryajf-world/go-ldap-admin/middleware"
"github.com/eryajf-world/go-ldap-admin/public/common"
"github.com/gin-gonic/gin"
)
// 初始化
func InitRoutes() *gin.Engine {
//设置模式
gin.SetMode(config.Conf.System.Mode)
// 创建带有默认中间件的路由:
// 日志与恢复中间件
r := gin.Default()
// 创建不带中间件的路由:
// r := gin.New()
// r.Use(gin.Recovery())
// 启用限流中间件
// 默认每50毫秒填充一个令牌最多填充200个
fillInterval := time.Duration(config.Conf.RateLimit.FillInterval)
capacity := config.Conf.RateLimit.Capacity
r.Use(middleware.RateLimitMiddleware(time.Millisecond*fillInterval, capacity))
// 启用全局跨域中间件
r.Use(middleware.CORSMiddleware())
// 启用操作日志中间件
r.Use(middleware.OperationLogMiddleware())
// 初始化JWT认证中间件
authMiddleware, err := middleware.InitAuth()
if err != nil {
common.Log.Panicf("初始化JWT中间件失败%v", err)
panic(fmt.Sprintf("初始化JWT中间件失败%v", err))
}
// 路由分组
apiGroup := r.Group("/" + config.Conf.System.UrlPathPrefix)
// 注册路由
InitBaseRoutes(apiGroup, authMiddleware) // 注册基础路由, 不需要jwt认证中间件,不需要casbin中间件
InitUserRoutes(apiGroup, authMiddleware) // 注册用户路由, jwt认证中间件,casbin鉴权中间件
InitGroupRoutes(apiGroup, authMiddleware) // 注册分组路由, jwt认证中间件,casbin鉴权中间件
InitRoleRoutes(apiGroup, authMiddleware) // 注册角色路由, jwt认证中间件,casbin鉴权中间件
InitMenuRoutes(apiGroup, authMiddleware) // 注册菜单路由, jwt认证中间件,casbin鉴权中间件
InitApiRoutes(apiGroup, authMiddleware) // 注册接口路由, jwt认证中间件,casbin鉴权中间件
InitOperationLogRoutes(apiGroup, authMiddleware) // 注册操作日志路由, jwt认证中间件,casbin鉴权中间件
common.Log.Info("初始化路由完成!")
return r
}

26
routes/api_routes.go Normal file
View File

@ -0,0 +1,26 @@
package routes
import (
"github.com/eryajf-world/go-ldap-admin/controller"
"github.com/eryajf-world/go-ldap-admin/middleware"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
)
func InitApiRoutes(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) gin.IRoutes {
api := r.Group("/api")
// 开启jwt认证中间件
api.Use(authMiddleware.MiddlewareFunc())
// 开启casbin鉴权中间件
api.Use(middleware.CasbinMiddleware())
{
api.GET("/tree", controller.Api.GetTree)
api.GET("/list", controller.Api.List)
api.POST("/add", controller.Api.Add)
api.POST("/update", controller.Api.Update)
api.POST("/delete", controller.Api.Delete)
}
return r
}

23
routes/base_routes.go Normal file
View File

@ -0,0 +1,23 @@
package routes
import (
"github.com/eryajf-world/go-ldap-admin/controller"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
)
// 注册基础路由
func InitBaseRoutes(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) gin.IRoutes {
base := r.Group("/base")
{
base.GET("ping", controller.Demo)
// 登录登出刷新token无需鉴权
base.POST("/login", authMiddleware.LoginHandler)
base.POST("/logout", authMiddleware.LogoutHandler)
base.POST("/refreshToken", authMiddleware.RefreshHandler)
base.POST("/changePwd", controller.Base.ChangePwd) // 修改用户密码
base.GET("/dashboard", controller.Base.Dashboard) // 系统首页展示数据
}
return r
}

30
routes/group_routes.go Normal file
View File

@ -0,0 +1,30 @@
package routes
import (
"github.com/eryajf-world/go-ldap-admin/controller"
"github.com/eryajf-world/go-ldap-admin/middleware"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
)
func InitGroupRoutes(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) gin.IRoutes {
group := r.Group("/group")
// 开启jwt认证中间件
group.Use(authMiddleware.MiddlewareFunc())
// 开启casbin鉴权中间件
group.Use(middleware.CasbinMiddleware())
{
group.GET("/list", controller.Group.List)
group.POST("/add", controller.Group.Add)
group.POST("/update", controller.Group.Update)
group.POST("/delete", controller.Group.Delete)
group.POST("/adduser", controller.Group.AddUser)
group.POST("/removeuser", controller.Group.RemoveUser)
group.GET("/useringroup", controller.Group.UserInGroup)
group.GET("/usernoingroup", controller.Group.UserNoInGroup)
}
return r
}

26
routes/menu_routes.go Normal file
View File

@ -0,0 +1,26 @@
package routes
import (
"github.com/eryajf-world/go-ldap-admin/controller"
"github.com/eryajf-world/go-ldap-admin/middleware"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
)
func InitMenuRoutes(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) gin.IRoutes {
menu := r.Group("/menu")
// 开启jwt认证中间件
menu.Use(authMiddleware.MiddlewareFunc())
// 开启casbin鉴权中间件
menu.Use(middleware.CasbinMiddleware())
{
menu.GET("/tree", controller.Menu.GetTree)
// menu.GET("/list", controller.Menu.List)
menu.POST("/add", controller.Menu.Add)
menu.POST("/update", controller.Menu.Update)
menu.POST("/delete", controller.Menu.Delete)
}
return r
}

View File

@ -0,0 +1,22 @@
package routes
import (
"github.com/eryajf-world/go-ldap-admin/controller"
"github.com/eryajf-world/go-ldap-admin/middleware"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
)
func InitOperationLogRoutes(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) gin.IRoutes {
operation_log := r.Group("/log")
// 开启jwt认证中间件
operation_log.Use(authMiddleware.MiddlewareFunc())
// 开启casbin鉴权中间件
operation_log.Use(middleware.CasbinMiddleware())
{
operation_log.GET("/operation/list", controller.OperationLog.List)
operation_log.POST("/operation/delete", controller.OperationLog.Delete)
}
return r
}

29
routes/role_routes.go Normal file
View File

@ -0,0 +1,29 @@
package routes
import (
"github.com/eryajf-world/go-ldap-admin/controller"
"github.com/eryajf-world/go-ldap-admin/middleware"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
)
func InitRoleRoutes(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) gin.IRoutes {
role := r.Group("/role")
// 开启jwt认证中间件
role.Use(authMiddleware.MiddlewareFunc())
// 开启casbin鉴权中间件
role.Use(middleware.CasbinMiddleware())
{
role.GET("/list", controller.Role.List)
role.POST("/add", controller.Role.Add)
role.POST("/update", controller.Role.Update)
role.POST("/delete", controller.Role.Delete)
role.GET("/getmenulist", controller.Role.GetMenuList)
role.GET("/getapilist", controller.Role.GetApiList)
role.POST("/updatemenus", controller.Role.UpdateMenus)
role.POST("/updateapis", controller.Role.UpdateApis)
}
return r
}

28
routes/user_routes.go Normal file
View File

@ -0,0 +1,28 @@
package routes
import (
"github.com/eryajf-world/go-ldap-admin/controller"
"github.com/eryajf-world/go-ldap-admin/middleware"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
)
// 注册用户路由
func InitUserRoutes(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) gin.IRoutes {
user := r.Group("/user")
// 开启jwt认证中间件
user.Use(authMiddleware.MiddlewareFunc())
// 开启casbin鉴权中间件
user.Use(middleware.CasbinMiddleware())
{
user.GET("/info", controller.User.GetUserInfo) // 暂时未完成
user.GET("/list", controller.User.List) // 用户列表
user.POST("/add", controller.User.Add) // 添加用户
user.POST("/update", controller.User.Update) // 更新用户
user.POST("/delete", controller.User.Delete) // 删除用户
user.POST("/changePwd", controller.User.ChangePwd) // 修改用户密码
user.POST("/changeUserStatus", controller.User.ChangeUserStatus) // 修改用户状态
}
return r
}

6
service/ildap/a_ildap.go Normal file
View File

@ -0,0 +1,6 @@
package ildap
var (
User = &UserService{}
Group = &GroupService{}
)

View File

@ -0,0 +1,58 @@
package ildap
import (
"fmt"
"github.com/eryajf-world/go-ldap-admin/config"
"github.com/eryajf-world/go-ldap-admin/model"
"github.com/eryajf-world/go-ldap-admin/public/common"
ldap "github.com/go-ldap/ldap/v3"
)
type GroupService struct{}
// Add 添加资源
func (x GroupService) Add(g *model.Group) error {
add := ldap.NewAddRequest(fmt.Sprintf("cn=%s,%s", g.GroupName, config.Conf.Ldap.LdapGroupDN), nil)
add.Attribute("objectClass", []string{"groupOfNames", "top"}) // 如果定义了 groupOfNAmes那么必须指定member否则报错如下object class 'groupOfNames' requires attribute 'member'
add.Attribute("cn", []string{g.GroupName})
add.Attribute("description", []string{g.Remark})
add.Attribute("member", []string{config.Conf.Ldap.LdapAdminDN}) // 所以这里创建组的时候默认将admin加入其中以免创建时没有人员而报上边的错误
return common.LDAP.Add(add)
}
// UpdateGroup 更新一个分组
func (x GroupService) Update(g *model.Group) error {
modify := ldap.NewModifyRequest(fmt.Sprintf("cn=%s,%s", g.GroupName, config.Conf.Ldap.LdapGroupDN), nil)
modify.Replace("description", []string{g.Remark})
return common.LDAP.Modify(modify)
}
// Delete 删除资源
func (x GroupService) Delete(group string) error {
del := ldap.NewDelRequest(fmt.Sprintf("cn=%s,%s", group, config.Conf.Ldap.LdapGroupDN), nil)
return common.LDAP.Del(del)
}
// AddUserToGroup 添加用户到分组
func (x GroupService) AddUserToGroup(group, user string) error {
udn := fmt.Sprintf("uid=%s,%s", user, config.Conf.Ldap.LdapUserDN)
if user == "admin" {
udn = config.Conf.Ldap.LdapAdminDN
}
gdn := fmt.Sprintf("cn=%s,%s", group, config.Conf.Ldap.LdapGroupDN)
newmr := ldap.NewModifyRequest(gdn, nil)
newmr.Add("member", []string{udn})
return common.LDAP.Modify(newmr)
}
// DelUserFromGroup 将用户从分组删除
func (x GroupService) RemoveUserFromGroup(group, user string) error {
udn := fmt.Sprintf("uid=%s,%s", user, config.Conf.Ldap.LdapUserDN)
gdn := fmt.Sprintf("cn=%s,%s", group, config.Conf.Ldap.LdapGroupDN)
newmr := ldap.NewModifyRequest(gdn, nil)
newmr.Delete("member", []string{udn})
return common.LDAP.Modify(newmr)
}

101
service/ildap/user_ildap.go Normal file
View File

@ -0,0 +1,101 @@
package ildap
import (
"fmt"
"github.com/eryajf-world/go-ldap-admin/config"
"github.com/eryajf-world/go-ldap-admin/model"
"github.com/eryajf-world/go-ldap-admin/public/common"
ldap "github.com/go-ldap/ldap/v3"
)
type UserService struct{}
// 创建资源
func (x UserService) Add(user *model.User) error {
if user.Departments == "" {
user.Departments = "研发中心"
}
if user.GivenName == "" {
user.GivenName = user.Nickname
}
if user.PostalAddress == "" {
user.PostalAddress = "没有填写地址"
}
if user.Position == "" {
user.Position = "技术"
}
if user.Introduction == "" {
user.Introduction = user.Nickname
}
add := ldap.NewAddRequest(fmt.Sprintf("uid=%s,%s", user.Username, config.Conf.Ldap.LdapUserDN), nil)
add.Attribute("objectClass", []string{"inetOrgPerson"})
add.Attribute("cn", []string{user.Nickname})
add.Attribute("sn", []string{user.Username})
add.Attribute("businessCategory", []string{user.Departments})
add.Attribute("departmentNumber", []string{user.Position})
add.Attribute("description", []string{user.Introduction})
add.Attribute("displayName", []string{user.Nickname})
add.Attribute("mail", []string{user.Mail})
add.Attribute("employeeNumber", []string{user.JobNumber})
add.Attribute("givenName", []string{user.GivenName})
add.Attribute("postalAddress", []string{user.PostalAddress})
add.Attribute("mobile", []string{user.Mobile})
add.Attribute("uid", []string{user.Username})
add.Attribute("userPassword", []string{user.Password})
return common.LDAP.Add(add)
}
// Update 更新资源
func (x UserService) Update(oldusername string, user *model.User) error {
modify := ldap.NewModifyRequest(fmt.Sprintf("uid=%s,%s", oldusername, config.Conf.Ldap.LdapUserDN), nil)
modify.Replace("cn", []string{user.Nickname})
modify.Replace("sn", []string{oldusername})
modify.Replace("businessCategory", []string{user.Departments})
modify.Replace("departmentNumber", []string{user.Position})
modify.Replace("description", []string{user.Introduction})
modify.Replace("displayName", []string{user.Nickname})
modify.Replace("mail", []string{user.Mail})
modify.Replace("employeeNumber", []string{user.JobNumber})
modify.Replace("givenName", []string{user.GivenName})
modify.Replace("postalAddress", []string{user.PostalAddress})
modify.Replace("mobile", []string{user.Mobile})
modify.Replace("uid", []string{oldusername})
return common.LDAP.Modify(modify)
}
// Delete 删除资源
func (x UserService) Delete(username string) error {
del := ldap.NewDelRequest(fmt.Sprintf("uid=%s,%s", username, config.Conf.Ldap.LdapUserDN), nil)
return common.LDAP.Del(del)
}
// ChangePwd 修改用户密码此处旧密码也可以为空ldap可以直接通过用户DN加上新密码来进行修改
func (u UserService) ChangePwd(username, oldpasswd, newpasswd string) error {
udn := fmt.Sprintf("uid=%s,%s", username, config.Conf.Ldap.LdapUserDN)
if username == "admin" {
udn = config.Conf.Ldap.LdapAdminDN
}
modifyPass := ldap.NewPasswordModifyRequest(udn, oldpasswd, newpasswd)
_, err := common.LDAP.PasswordModify(modifyPass)
if err != nil {
return fmt.Errorf("password modify failed for %s, err: %v", username, err)
}
return nil
}
// NewPwd 新旧密码都是空,通过管理员可以修改成功并返回新的密码
func (x UserService) NewPwd(username string) (string, error) {
udn := fmt.Sprintf("uid=%s,%s", username, config.Conf.Ldap.LdapUserDN)
if username == "admin" {
udn = config.Conf.Ldap.LdapAdminDN
}
modifyPass := ldap.NewPasswordModifyRequest(udn, "", "")
newpass, err := common.LDAP.PasswordModify(modifyPass)
if err != nil {
return "", fmt.Errorf("password modify failed for %s, err: %v", username, err)
}
return newpass.GeneratedPassword, nil
}

10
service/isql/a_isql.go Normal file
View File

@ -0,0 +1,10 @@
package isql
var (
User = &UserService{}
Group = &GroupService{}
Api = &ApiService{}
Menu = &MenuService{}
Role = &RoleService{}
OperationLog = &OperationLogService{}
)

191
service/isql/api_isql.go Normal file
View File

@ -0,0 +1,191 @@
package isql
import (
"errors"
"fmt"
"strings"
"github.com/eryajf-world/go-ldap-admin/model"
"github.com/eryajf-world/go-ldap-admin/public/common"
"github.com/eryajf-world/go-ldap-admin/public/tools"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"gorm.io/gorm"
)
type ApiService struct{}
// List 获取数据列表
func (s ApiService) List(req *request.ApiListReq) ([]*model.Api, error) {
var list []*model.Api
db := common.DB.Model(&model.Api{}).Order("created_at DESC")
method := strings.TrimSpace(req.Method)
if method != "" {
db = db.Where("method LIKE ?", fmt.Sprintf("%%%s%%", method))
}
path := strings.TrimSpace(req.Path)
if path != "" {
db = db.Where("path LIKE ?", fmt.Sprintf("%%%s%%", path))
}
category := strings.TrimSpace(req.Category)
if category != "" {
db = db.Where("category LIKE ?", fmt.Sprintf("%%%s%%", category))
}
creator := strings.TrimSpace(req.Creator)
if creator != "" {
db = db.Where("creator LIKE ?", fmt.Sprintf("%%%s%%", creator))
}
pageReq := tools.NewPageOption(req.PageNum, req.PageSize)
err := db.Offset(pageReq.PageNum).Limit(pageReq.PageSize).Find(&list).Error
return list, err
}
// List 获取数据列表
func (s ApiService) ListAll() (list []*model.Api, err error) {
err = common.DB.Model(&model.Api{}).Order("created_at DESC").Find(&list).Error
return list, err
}
// Count 获取数据总数
func (s ApiService) Count() (int64, error) {
var count int64
err := common.DB.Model(&model.Api{}).Count(&count).Error
return count, err
}
// Add 添加资源
func (s ApiService) Add(api *model.Api) error {
return common.DB.Create(api).Error
}
// Update 更新资源
func (s ApiService) Update(api *model.Api) error {
// 根据id获取接口信息
var oldApi model.Api
err := common.DB.First(&oldApi, api.ID).Error
if err != nil {
return errors.New("根据接口ID获取接口信息失败")
}
err = common.DB.Model(api).Where("id = ?", api.ID).Updates(api).Error
if err != nil {
return err
}
// 更新了method和path就更新casbin中policy
if oldApi.Path != api.Path || oldApi.Method != api.Method {
policies := common.CasbinEnforcer.GetFilteredPolicy(1, oldApi.Path, oldApi.Method)
// 接口在casbin的policy中存在才进行操作
if len(policies) > 0 {
// 先删除
isRemoved, _ := common.CasbinEnforcer.RemovePolicies(policies)
if !isRemoved {
return errors.New("更新权限接口失败")
}
for _, policy := range policies {
policy[1] = api.Path
policy[2] = api.Method
}
// 新增
isAdded, _ := common.CasbinEnforcer.AddPolicies(policies)
if !isAdded {
return errors.New("更新权限接口失败")
}
// 加载policy
err := common.CasbinEnforcer.LoadPolicy()
if err != nil {
return errors.New("更新权限接口成功,权限接口策略加载失败")
} else {
return err
}
}
}
return err
}
// Find 获取单个资源
func (s ApiService) Find(filter map[string]interface{}, data *model.Api) error {
return common.DB.Where(filter).First(&data).Error
}
// Exist 判断资源是否存在
func (s ApiService) Exist(filter map[string]interface{}) bool {
var dataObj model.Api
err := common.DB.Debug().Order("created_at DESC").Where(filter).First(&dataObj).Error
return !errors.Is(err, gorm.ErrRecordNotFound)
}
// Delete 批量删除
func (s ApiService) Delete(ids []uint) error {
var apis []model.Api
for _, id := range ids {
// 根据ID获取用户
api := new(model.Api)
err := s.Find(tools.H{"id": id}, api)
if err != nil {
return errors.New(fmt.Sprintf("未获取到ID为%d的用户", id))
}
apis = append(apis, *api)
}
err := common.DB.Where("id IN (?)", ids).Unscoped().Delete(&model.Api{}).Error
// 如果删除成功删除casbin中policy
if err == nil {
for _, api := range apis {
policies := common.CasbinEnforcer.GetFilteredPolicy(1, api.Path, api.Method)
if len(policies) > 0 {
isRemoved, _ := common.CasbinEnforcer.RemovePolicies(policies)
if !isRemoved {
return errors.New("删除权限接口失败")
}
}
}
// 重新加载策略
err := common.CasbinEnforcer.LoadPolicy()
if err != nil {
return errors.New("删除权限接口成功,权限接口策略加载失败")
} else {
return err
}
}
return err
}
// // GetApiTree 获取接口树(按接口Category字段分类)
// func (s ApiService) GetApiTree() ([]*response.ApiTreeDto, error) {
// var apiList []*model.Api
// err := common.DB.Order("category").Order("created_at").Find(&apiList).Error
// // 获取所有的分类
// var categoryList []string
// for _, api := range apiList {
// categoryList = append(categoryList, api.Category)
// }
// // 获取去重后的分类
// categoryUniq := funk.UniqString(categoryList)
// apiTree := make([]*response.ApiTreeDto, len(categoryUniq))
// for i, category := range categoryUniq {
// apiTree[i] = &response.ApiTreeDto{
// ID: -i,
// Desc: category,
// Category: category,
// Children: nil,
// }
// for _, api := range apiList {
// if category == api.Category {
// apiTree[i].Children = append(apiTree[i].Children, api)
// }
// }
// }
// return apiTree, err
// }
// GetApisById 根据接口ID获取接口列表
func (s ApiService) GetApisById(apiIds []uint) ([]*model.Api, error) {
var apis []*model.Api
err := common.DB.Where("id IN (?)", apiIds).Find(&apis).Error
return apis, err
}

View File

@ -0,0 +1,85 @@
package isql
import (
"errors"
"fmt"
"strings"
"github.com/eryajf-world/go-ldap-admin/model"
"github.com/eryajf-world/go-ldap-admin/public/common"
"github.com/eryajf-world/go-ldap-admin/public/tools"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"gorm.io/gorm"
)
type GroupService struct{}
// List 获取数据列表
func (s GroupService) List(req *request.GroupListReq) ([]*model.Group, error) {
var list []*model.Group
db := common.DB.Model(&model.Group{}).Order("created_at DESC")
groupName := strings.TrimSpace(req.GroupName)
if groupName != "" {
db = db.Where("group_name LIKE ?", fmt.Sprintf("%%%s%%", groupName))
}
groupRemark := strings.TrimSpace(req.Remark)
if groupRemark != "" {
db = db.Where("remark LIKE ?", fmt.Sprintf("%%%s%%", groupRemark))
}
pageReq := tools.NewPageOption(req.PageNum, req.PageSize)
err := db.Offset(pageReq.PageNum).Limit(pageReq.PageSize).Preload("Users").Find(&list).Error
return list, err
}
// Count 获取数据总数
func (s GroupService) Count() (int64, error) {
var count int64
err := common.DB.Model(&model.Group{}).Count(&count).Error
return count, err
}
// Add 添加资源
func (s GroupService) Add(data *model.Group) error {
return common.DB.Create(data).Error
}
// Update 更新资源
func (s GroupService) Update(dataObj *model.Group) error {
return common.DB.Model(dataObj).Where("id = ?", dataObj.ID).Updates(dataObj).Error
}
// Find 获取单个资源
func (s GroupService) Find(filter map[string]interface{}, data *model.Group) error {
return common.DB.Where(filter).Preload("Users").First(&data).Error
}
// Exist 判断资源是否存在
func (s GroupService) Exist(filter map[string]interface{}) bool {
var dataObj model.Group
err := common.DB.Debug().Order("created_at DESC").Where(filter).First(&dataObj).Error
return !errors.Is(err, gorm.ErrRecordNotFound)
}
// Delete 批量删除
func (s GroupService) Delete(ids []uint) error {
return common.DB.Where("id IN (?)", ids).Select("Users").Unscoped().Delete(&model.Group{}).Error
}
// GetApisById 根据接口ID获取接口列表
func (s GroupService) GetGroupByIds(ids []uint) (datas []*model.Group, err error) {
err = common.DB.Where("id IN (?)", ids).Preload("Users").Find(&datas).Error
return datas, err
}
// AddUserToGroup 添加用户到分组
func (s GroupService) AddUserToGroup(group *model.Group, users []model.User) error {
return common.DB.Model(&group).Association("Users").Append(users)
}
// RemoveUserFromGroup 将用户从分组移除
func (s GroupService) RemoveUserFromGroup(group *model.Group, users []model.User) error {
return common.DB.Model(&group).Association("Users").Delete(users)
}

125
service/isql/menu_isql.go Normal file
View File

@ -0,0 +1,125 @@
package isql
import (
"errors"
"github.com/eryajf-world/go-ldap-admin/model"
"github.com/eryajf-world/go-ldap-admin/public/common"
"github.com/thoas/go-funk"
"gorm.io/gorm"
)
type MenuService struct{}
// Exist 判断资源是否存在
func (s MenuService) Exist(filter map[string]interface{}) bool {
var dataObj model.Menu
err := common.DB.Debug().Order("created_at DESC").Where(filter).First(&dataObj).Error
return !errors.Is(err, gorm.ErrRecordNotFound)
}
// Count 获取资源总数
func (s MenuService) Count() (int64, error) {
var count int64
err := common.DB.Model(&model.Menu{}).Count(&count).Error
return count, err
}
// Add 创建资源
func (s MenuService) Add(menu *model.Menu) error {
return common.DB.Create(menu).Error
}
// Update 更新资源
func (s MenuService) Update(menu *model.Menu) error {
return common.DB.Model(&model.Menu{}).Where("id = ?", menu.ID).Updates(menu).Error
}
// Find 获取单个资源
func (s MenuService) Find(filter map[string]interface{}, data *model.Menu) error {
return common.DB.Where(filter).First(&data).Error
}
// List 获取数据列表
func (s MenuService) List() (menus []*model.Menu, err error) {
err = common.DB.Order("sort").Find(&menus).Error
return menus, err
}
// 批量删除资源
func (s MenuService) Delete(menuIds []uint) error {
return common.DB.Where("id IN (?)", menuIds).Select("Roles").Unscoped().Delete(&model.Menu{}).Error
}
// GetUserMenusByUserId 根据用户ID获取用户的权限(可访问)菜单列表
func (s MenuService) GetUserMenusByUserId(userId uint) ([]*model.Menu, error) {
// 获取用户
var user model.User
err := common.DB.Where("id = ?", userId).Preload("Roles").First(&user).Error
if err != nil {
return nil, err
}
// 获取角色
roles := user.Roles
// 所有角色的菜单集合
allRoleMenus := make([]*model.Menu, 0)
for _, role := range roles {
var userRole model.Role
err := common.DB.Where("id = ?", role.ID).Preload("Menus").First(&userRole).Error
if err != nil {
return nil, err
}
// 获取角色的菜单
menus := userRole.Menus
allRoleMenus = append(allRoleMenus, menus...)
}
// 所有角色的菜单集合去重
allRoleMenusId := make([]int, 0)
for _, menu := range allRoleMenus {
allRoleMenusId = append(allRoleMenusId, int(menu.ID))
}
allRoleMenusIdUniq := funk.UniqInt(allRoleMenusId)
allRoleMenusUniq := make([]*model.Menu, 0)
for _, id := range allRoleMenusIdUniq {
for _, menu := range allRoleMenus {
if id == int(menu.ID) {
allRoleMenusUniq = append(allRoleMenusUniq, menu)
break
}
}
}
// 获取状态status为1的菜单
accessMenus := make([]*model.Menu, 0)
for _, menu := range allRoleMenusUniq {
if menu.Status == 1 {
accessMenus = append(accessMenus, menu)
}
}
return accessMenus, err
}
// GenMenuTree 生成菜单树
func GenMenuTree(parentId uint, menus []*model.Menu) []*model.Menu {
tree := make([]*model.Menu, 0)
for _, m := range menus {
if m.ParentId == parentId {
children := GenMenuTree(m.ID, menus)
m.Children = children
tree = append(tree, m)
}
}
return tree
}
// // GetMenuTree 获取菜单树
// func (s MenuService) GetMenuTree() ([]*model.Menu, error) {
// var menus []*model.Menu
// err := common.DB.Order("sort").Find(&menus).Error
// // parentId为0的是根菜单
// return GenMenuTree(0, menus), err
// }

View File

@ -0,0 +1,84 @@
package isql
import (
"errors"
"fmt"
"strings"
"github.com/eryajf-world/go-ldap-admin/model"
"github.com/eryajf-world/go-ldap-admin/public/common"
"github.com/eryajf-world/go-ldap-admin/public/tools"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"gorm.io/gorm"
)
type OperationLogService struct{}
//var Logs []model.OperationLog //全局变量多个线程需要加锁,所以每个线程自己维护一个
//处理OperationLogChan将日志记录到数据库
func (s OperationLogService) SaveOperationLogChannel(olc <-chan *model.OperationLog) {
// 只会在线程开启的时候执行一次
Logs := make([]model.OperationLog, 0)
// 一直执行--收到olc就会执行
for log := range olc {
Logs = append(Logs, *log)
// 每10条记录到数据库
if len(Logs) > 5 {
common.DB.Create(&Logs)
Logs = make([]model.OperationLog, 0)
}
}
}
// List 获取数据列表
func (s OperationLogService) List(req *request.OperationLogListReq) ([]*model.OperationLog, error) {
var list []*model.OperationLog
db := common.DB.Model(&model.OperationLog{}).Order("start_time DESC")
username := strings.TrimSpace(req.Username)
if username != "" {
db = db.Where("username LIKE ?", fmt.Sprintf("%%%s%%", username))
}
ip := strings.TrimSpace(req.Ip)
if ip != "" {
db = db.Where("ip LIKE ?", fmt.Sprintf("%%%s%%", ip))
}
path := strings.TrimSpace(req.Path)
if path != "" {
db = db.Where("path LIKE ?", fmt.Sprintf("%%%s%%", path))
}
status := req.Status
if status != 0 {
db = db.Where("status = ?", status)
}
pageReq := tools.NewPageOption(req.PageNum, req.PageSize)
err := db.Offset(pageReq.PageNum).Limit(pageReq.PageSize).Find(&list).Error
return list, err
}
// Count 获取数据总数
func (s OperationLogService) Count() (count int64, err error) {
err = common.DB.Model(&model.OperationLog{}).Count(&count).Error
return count, err
}
// 获取单个用户
func (s OperationLogService) Find(filter map[string]interface{}, data *model.OperationLog) error {
return common.DB.Where(filter).First(&data).Error
}
// Exist 判断资源是否存在
func (s OperationLogService) Exist(filter map[string]interface{}) bool {
var dataObj model.OperationLog
err := common.DB.Debug().Order("created_at DESC").Where(filter).First(&dataObj).Error
return !errors.Is(err, gorm.ErrRecordNotFound)
}
// Delete 删除资源
func (s OperationLogService) Delete(operationLogIds []uint) error {
return common.DB.Where("id IN (?)", operationLogIds).Unscoped().Delete(&model.OperationLog{}).Error
}

138
service/isql/role_isql.go Normal file
View File

@ -0,0 +1,138 @@
package isql
import (
"errors"
"fmt"
"strings"
"github.com/eryajf-world/go-ldap-admin/model"
"github.com/eryajf-world/go-ldap-admin/public/common"
"github.com/eryajf-world/go-ldap-admin/public/tools"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"gorm.io/gorm"
)
type RoleService struct{}
// Exist 判断资源是否存在
func (s RoleService) Exist(filter map[string]interface{}) bool {
var dataObj model.Role
err := common.DB.Debug().Order("created_at DESC").Where(filter).First(&dataObj).Error
return !errors.Is(err, gorm.ErrRecordNotFound)
}
// List 获取数据列表
func (s RoleService) List(req *request.RoleListReq) ([]*model.Role, error) {
var list []*model.Role
db := common.DB.Model(&model.Role{}).Order("created_at DESC")
name := strings.TrimSpace(req.Name)
if name != "" {
db = db.Where("name LIKE ?", fmt.Sprintf("%%%s%%", name))
}
keyword := strings.TrimSpace(req.Keyword)
if keyword != "" {
db = db.Where("keyword LIKE ?", fmt.Sprintf("%%%s%%", keyword))
}
status := req.Status
if status != 0 {
db = db.Where("status = ?", status)
}
pageReq := tools.NewPageOption(req.PageNum, req.PageSize)
err := db.Offset(pageReq.PageNum).Limit(pageReq.PageSize).Find(&list).Error
return list, err
}
// Count 获取资源总数
func (s RoleService) Count() (int64, error) {
var count int64
err := common.DB.Model(&model.Role{}).Count(&count).Error
return count, err
}
// Add 创建资源
func (s RoleService) Add(role *model.Role) error {
return common.DB.Create(role).Error
}
// Update 更新资源
func (s RoleService) Update(role *model.Role) error {
return common.DB.Model(&model.Role{}).Where("id = ?", role.ID).Updates(role).Error
}
// Find 获取单个资源
func (s RoleService) Find(filter map[string]interface{}, data *model.Role) error {
return common.DB.Where(filter).First(&data).Error
}
// Delete 删除资源
func (s RoleService) Delete(roleIds []uint) error {
var roles []*model.Role
err := common.DB.Where("id IN (?)", roleIds).Find(&roles).Error
if err != nil {
return err
}
err = common.DB.Select("Users", "Menus").Unscoped().Delete(&roles).Error
// 删除成功就删除casbin policy
if err == nil {
for _, role := range roles {
roleKeyword := role.Keyword
rmPolicies := common.CasbinEnforcer.GetFilteredPolicy(0, roleKeyword)
if len(rmPolicies) > 0 {
isRemoved, _ := common.CasbinEnforcer.RemovePolicies(rmPolicies)
if !isRemoved {
return errors.New("删除角色成功, 删除角色关联权限接口失败")
}
}
}
}
return err
}
// Delete 根据角色ID获取角色
func (s RoleService) GetRolesByIds(roleIds []uint) ([]*model.Role, error) {
var list []*model.Role
err := common.DB.Where("id IN (?)", roleIds).Find(&list).Error
return list, err
}
// GetRoleMenusById 获取角色的权限菜单
func (s RoleService) GetRoleMenusById(roleId uint) ([]*model.Menu, error) {
var role model.Role
err := common.DB.Where("id = ?", roleId).Preload("Menus").First(&role).Error
return role.Menus, err
}
// UpdateRoleMenus 更新角色的权限菜单
func (s RoleService) UpdateRoleMenus(role *model.Role) error {
return common.DB.Model(role).Association("Menus").Replace(role.Menus)
}
// UpdateRoleApis 更新角色的权限接口(先全部删除再新增)
func (s RoleService) UpdateRoleApis(roleKeyword string, reqRolePolicies [][]string) error {
// 先获取path中的角色ID对应角色已有的police(需要先删除的)
err := common.CasbinEnforcer.LoadPolicy()
if err != nil {
return errors.New("角色的权限接口策略加载失败")
}
rmPolicies := common.CasbinEnforcer.GetFilteredPolicy(0, roleKeyword)
if len(rmPolicies) > 0 {
isRemoved, _ := common.CasbinEnforcer.RemovePolicies(rmPolicies)
if !isRemoved {
return errors.New("更新角色的权限接口失败")
}
}
isAdded, _ := common.CasbinEnforcer.AddPolicies(reqRolePolicies)
if !isAdded {
return errors.New("更新角色的权限接口失败")
}
err = common.CasbinEnforcer.LoadPolicy()
if err != nil {
return errors.New("更新角色的权限接口成功,角色的权限接口策略加载失败")
} else {
return err
}
}

290
service/isql/user_isql.go Normal file
View File

@ -0,0 +1,290 @@
package isql
import (
"errors"
"fmt"
"strings"
"time"
"github.com/eryajf-world/go-ldap-admin/model"
"github.com/eryajf-world/go-ldap-admin/public/common"
"github.com/eryajf-world/go-ldap-admin/public/tools"
"github.com/eryajf-world/go-ldap-admin/svc/request"
"github.com/gin-gonic/gin"
"github.com/patrickmn/go-cache"
"github.com/thoas/go-funk"
"gorm.io/gorm"
)
type UserService struct{}
// 当前用户信息缓存,避免频繁获取数据库
var userInfoCache = cache.New(24*time.Hour, 48*time.Hour)
// Add 添加资源
func (s UserService) Add(user *model.User) error {
user.Password = tools.NewGenPasswd(user.Password)
return common.DB.Create(user).Error
}
// List 获取数据列表
func (s UserService) List(req *request.UserListReq) ([]*model.User, error) {
var list []*model.User
db := common.DB.Model(&model.User{}).Order("id DESC")
username := strings.TrimSpace(req.Username)
if username != "" {
db = db.Where("username LIKE ?", fmt.Sprintf("%%%s%%", username))
}
nickname := strings.TrimSpace(req.Nickname)
if nickname != "" {
db = db.Where("nickname LIKE ?", fmt.Sprintf("%%%s%%", nickname))
}
mobile := strings.TrimSpace(req.Mobile)
if mobile != "" {
db = db.Where("mobile LIKE ?", fmt.Sprintf("%%%s%%", mobile))
}
status := req.Status
if status != 0 {
db = db.Where("status = ?", status)
}
pageReq := tools.NewPageOption(req.PageNum, req.PageSize)
err := db.Offset(pageReq.PageNum).Limit(pageReq.PageSize).Preload("Roles").Find(&list).Error
return list, err
}
// List 获取数据列表
func (s UserService) ListAll() (list []*model.User, err error) {
err = common.DB.Model(&model.User{}).Order("created_at DESC").Find(&list).Error
return list, err
}
// Count 获取数据总数
func (s UserService) Count() (int64, error) {
var count int64
err := common.DB.Model(&model.User{}).Count(&count).Error
return count, err
}
// Exist 判断资源是否存在
func (s UserService) Exist(filter map[string]interface{}) bool {
var dataObj model.User
err := common.DB.Where(filter).First(&dataObj).Error
return !errors.Is(err, gorm.ErrRecordNotFound)
}
// Find 获取单个资源
func (s UserService) Find(filter map[string]interface{}, data *model.User) error {
return common.DB.Where(filter).Preload("Roles").First(&data).Error
}
// Update 更新资源
func (s UserService) Update(user *model.User) error {
err := common.DB.Model(user).Updates(user).Error
if err != nil {
return err
}
err = common.DB.Model(user).Association("Roles").Replace(user.Roles)
// 如果更新成功就更新用户信息缓存
if err == nil {
userInfoCache.Set(user.Username, *user, cache.DefaultExpiration)
}
return err
}
// GetUserMinRoleSortsByIds 根据用户ID获取用户角色排序最小值
func (s UserService) GetUserMinRoleSortsByIds(ids []uint) ([]int, error) {
// 根据用户ID获取用户信息
var userList []model.User
err := common.DB.Where("id IN (?)", ids).Preload("Roles").Find(&userList).Error
if err != nil {
return []int{}, err
}
if len(userList) == 0 {
return []int{}, errors.New("未获取到任何用户信息")
}
var roleMinSortList []int
for _, user := range userList {
roles := user.Roles
var roleSortList []int
for _, role := range roles {
roleSortList = append(roleSortList, int(role.Sort))
}
roleMinSort := funk.MinInt(roleSortList).(int)
roleMinSortList = append(roleMinSortList, roleMinSort)
}
return roleMinSortList, nil
}
//GetCurrentUserMinRoleSort 获取当前用户角色排序最小值(最高等级角色)以及当前用户信息
func (s UserService) GetCurrentUserMinRoleSort(c *gin.Context) (uint, model.User, error) {
// 获取当前用户
ctxUser, err := s.GetCurrentLoginUser(c)
if err != nil {
return 999, ctxUser, err
}
// 获取当前用户的所有角色
currentRoles := ctxUser.Roles
// 获取当前用户角色的排序,和前端传来的角色排序做比较
var currentRoleSorts []int
for _, role := range currentRoles {
currentRoleSorts = append(currentRoleSorts, int(role.Sort))
}
// 当前用户角色排序最小值(最高等级角色)
currentRoleSortMin := uint(funk.MinInt(currentRoleSorts).(int))
return currentRoleSortMin, ctxUser, nil
}
// Delete 批量删除
func (s UserService) Delete(ids []uint) error {
// 用户和角色存在多对多关联关系
var users []model.User
for _, id := range ids {
// 根据ID获取用户
user := new(model.User)
err := s.Find(map[string]interface{}{"id": id}, user)
if err != nil {
return errors.New(fmt.Sprintf("未获取到ID为%d的用户", id))
}
users = append(users, *user)
}
err := common.DB.Select("Roles").Unscoped().Delete(&users).Error
// 删除用户成功,则删除用户信息缓存
if err == nil {
for _, user := range users {
userInfoCache.Delete(user.Username)
}
}
return err
}
// GetUserByIds 根据用户ID获取用户角色排序最小值
func (s UserService) GetUserByIds(ids []uint) ([]model.User, error) {
// 根据用户ID获取用户信息
var userList []model.User
err := common.DB.Where("id IN (?)", ids).Preload("Roles").Find(&userList).Error
return userList, err
}
// ChangePwd 更新密码
func (s UserService) ChangePwd(username string, hashNewPasswd string) error {
err := common.DB.Model(&model.User{}).Where("username = ?", username).Update("password", hashNewPasswd).Error
// 如果更新密码成功,则更新当前用户信息缓存
// 先获取缓存
cacheUser, found := userInfoCache.Get(username)
if err == nil {
if found {
user := cacheUser.(model.User)
user.Password = hashNewPasswd
userInfoCache.Set(username, user, cache.DefaultExpiration)
} else {
// 没有缓存就获取用户信息缓存
var user model.User
common.DB.Where("username = ?", username).First(&user)
userInfoCache.Set(username, user, cache.DefaultExpiration)
}
}
return err
}
// ChangeStatus 更新状态
func (s UserService) ChangeStatus(id, status int) error {
return common.DB.Model(&model.User{}).Where("id = ?", id).Update("status", status).Error
}
// GetCurrentLoginUser 获取当前登录用户信息
// 需要缓存,减少数据库访问
func (s UserService) GetCurrentLoginUser(c *gin.Context) (model.User, error) {
var newUser model.User
ctxUser, exist := c.Get("user")
if !exist {
return newUser, errors.New("用户未登录")
}
u, _ := ctxUser.(model.User)
// 先获取缓存
cacheUser, found := userInfoCache.Get(u.Username)
var user model.User
var err error
if found {
user = cacheUser.(model.User)
err = nil
} else {
// 缓存中没有就获取数据库
user, err = s.GetUserById(u.ID)
// 获取成功就缓存
if err != nil {
userInfoCache.Delete(u.Username)
} else {
userInfoCache.Set(u.Username, user, cache.DefaultExpiration)
}
}
return user, err
}
// Login 登录
func (s UserService) Login(user *model.User) (*model.User, error) {
// 根据用户名获取用户(正常状态:用户状态正常)
var firstUser model.User
// err := common.DB.
// Where("username = ?", user.Username).
// Preload("Roles").
// First(&firstUser).Error
// if err != nil {
// return nil, errors.New("用户不存在")
// }
err := s.Find(tools.H{"username": user.Username}, &firstUser)
if err != nil {
return nil, errors.New("用户不存在")
}
// 判断用户的状态
userStatus := firstUser.Status
if userStatus != 1 {
return nil, errors.New("用户被禁用")
}
// 判断用户拥有的所有角色的状态,全部角色都被禁用则不能登录
// roles := firstUser.Roles
// isValidate := false
// for _, role := range roles {
// // 有一个正常状态的角色就可以登录
// if role.Status == 1 {
// isValidate = true
// break
// }
// }
// if !isValidate {
// return nil, errors.New("用户角色被禁用")
// }
if tools.NewParPasswd(firstUser.Password) != user.Password {
return nil, errors.New("密码错误")
}
// 校验密码
// err = tools.ComparePasswd(firstUser.Password, user.Password)
// if err != nil {
// return &firstUser, errors.New("密码错误")
// }
return &firstUser, nil
}
// ClearUserInfoCache 清理所有用户信息缓存
func (s UserService) ClearUserInfoCache() {
userInfoCache.Flush()
}
// GetUserById 获取单个用户
func (us UserService) GetUserById(id uint) (model.User, error) {
var user model.User
err := common.DB.Where("id = ?", id).Preload("Roles").First(&user).Error
return user, err
}

37
svc/request/api_req.go Normal file
View File

@ -0,0 +1,37 @@
package request
// ApiListReq 获取资源列表结构体
type ApiListReq struct {
Method string `json:"method" form:"method"`
Path string `json:"path" form:"path"`
Category string `json:"category" form:"category"`
Creator string `json:"creator" form:"creator"`
PageNum int `json:"pageNum" form:"pageNum"`
PageSize int `json:"pageSize" form:"pageSize"`
}
// ApiAddReq 添加资源结构体
type ApiAddReq struct {
Method string `json:"method" validate:"required,min=1,max=20"`
Path string `json:"path" validate:"required,min=1,max=100"`
Category string `json:"category" validate:"required,min=1,max=50"`
Remark string `json:"remark" validate:"min=0,max=100"`
}
// ApiUpdateReq 更新资源结构体
type ApiUpdateReq struct {
ID uint `json:"id" validate:"required"`
Method string `json:"method" validate:"min=1,max=20"`
Path string `json:"path" validate:"min=1,max=100"`
Category string `json:"category" validate:"min=1,max=50"`
Remark string `json:"remark" validate:"min=0,max=100"`
}
// ApiDeleteReq 删除资源结构体
type ApiDeleteReq struct {
ApiIds []uint `json:"apiIds" validate:"required"`
}
// ApiGetTreeReq 获取资源树结构体
type ApiGetTreeReq struct {
}

10
svc/request/base_req.go Normal file
View File

@ -0,0 +1,10 @@
package request
// BaseChangePwdReq 修改密码结构体
type BaseChangePwdReq struct {
Mail string `json:"mail" validate:"required,min=0,max=20"`
}
// BaseDashboardReq 系统首页展示数据结构体
type BaseDashboardReq struct {
}

52
svc/request/group_req.go Normal file
View File

@ -0,0 +1,52 @@
package request
// GroupListReq 获取资源列表结构体
type GroupListReq struct {
GroupName string `json:"groupName" form:"groupName"`
Remark string `json:"remark" form:"remark"`
PageNum int `json:"pageNum" form:"pageNum"`
PageSize int `json:"pageSize" form:"pageSize"`
}
// GroupAddReq 添加资源结构体
type GroupAddReq struct {
GroupName string `json:"groupName" validate:"required,min=1,max=20"`
Remark string `json:"remark" validate:"min=0,max=100"` // 分组的中文描述
}
// GroupUpdateReq 更新资源结构体
type GroupUpdateReq struct {
ID uint `json:"id" form:"id" validate:"required"`
Remark string `json:"remark" validate:"min=0,max=100"` // 分组的中文描述
}
// GroupDeleteReq 删除资源结构体
type GroupDeleteReq struct {
GroupIds []uint `json:"groupIds" validate:"required"`
}
// GroupGetTreeReq 获取资源树结构体
type GroupGetTreeReq struct {
}
type GroupAddUserReq struct {
GroupID uint `json:"groupId" validate:"required"`
UserIds []uint `json:"userIds" validate:"required"`
}
type GroupRemoveUserReq struct {
GroupID uint `json:"groupId" validate:"required"`
UserIds []uint `json:"userIds" validate:"required"`
}
// UserInGroupReq 在分组内的用户
type UserInGroupReq struct {
GroupID uint `json:"groupId" form:"groupId" validate:"required"`
Nickname string `json:"nickname" form:"nickname"`
}
// UserNoInGroupReq 不在分组内的用户
type UserNoInGroupReq struct {
GroupID uint `json:"groupId" form:"groupId" validate:"required"`
Nickname string `json:"nickname" form:"nickname"`
}

51
svc/request/menu_req.go Normal file
View File

@ -0,0 +1,51 @@
package request
// MenuAddReq 添加资源结构体
type MenuAddReq struct {
Name string `json:"name" validate:"required,min=1,max=50"`
Title string `json:"title" validate:"required,min=1,max=50"`
Icon string `json:"icon" validate:"min=0,max=50"`
Path string `json:"path" validate:"required,min=1,max=100"`
Redirect string `json:"redirect" validate:"min=0,max=100"`
Component string `json:"component" validate:"required,min=1,max=100"`
Sort uint `json:"sort" validate:"gte=1,lte=999"`
Status uint `json:"status" validate:"oneof=1 2"`
Hidden uint `json:"hidden" validate:"oneof=1 2"`
NoCache uint `json:"noCache" validate:"oneof=1 2"`
AlwaysShow uint `json:"alwaysShow" validate:"oneof=1 2"`
Breadcrumb uint `json:"breadcrumb" validate:"oneof=1 2"`
ActiveMenu string `json:"activeMenu" validate:"min=0,max=100"`
ParentId uint `json:"parentId" validate:"required"`
}
// MenuListReq 列表结构体
type MenuListReq struct {
}
// MenuUpdateReq 更新资源结构体
type MenuUpdateReq struct {
ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"required,min=1,max=50"`
Title string `json:"title" validate:"required,min=1,max=50"`
Icon string `json:"icon" validate:"min=0,max=50"`
Path string `json:"path" validate:"required,min=1,max=100"`
Redirect string `json:"redirect" validate:"min=0,max=100"`
Component string `json:"component" validate:"min=0,max=100"`
Sort uint `json:"sort" validate:"gte=1,lte=999"`
Status uint `json:"status" validate:"oneof=1 2"`
Hidden uint `json:"hidden" validate:"oneof=1 2"`
NoCache uint `json:"noCache" validate:"oneof=1 2"`
AlwaysShow uint `json:"alwaysShow" validate:"oneof=1 2"`
Breadcrumb uint `json:"breadcrumb" validate:"oneof=1 2"`
ActiveMenu string `json:"activeMenu" validate:"min=0,max=100"`
ParentId uint `json:"parentId" validate:"required"`
}
// MenuDeleteReq 删除资源结构体
type MenuDeleteReq struct {
MenuIds []uint `json:"menuIds" validate:"required"`
}
// MenuGetTreeReq 获取菜单树结构体
type MenuGetTreeReq struct {
}

View File

@ -0,0 +1,16 @@
package request
// OperationLogListReq 操作日志请求结构体
type OperationLogListReq struct {
Username string `json:"username" form:"username"`
Ip string `json:"ip" form:"ip"`
Path string `json:"path" form:"path"`
Status int `json:"status" form:"status"`
PageNum int `json:"pageNum" form:"pageNum"`
PageSize int `json:"pageSize" form:"pageSize"`
}
// OperationLogDeleteReq 批量删除操作日志结构体
type OperationLogDeleteReq struct {
OperationLogIds []uint `json:"operationLogIds" validate:"required"`
}

60
svc/request/role_req.go Normal file
View File

@ -0,0 +1,60 @@
package request
// RoleAddReq 添加资源结构体
type RoleAddReq struct {
Name string `json:"name" validate:"required,min=1,max=20"`
Keyword string `json:"keyword" validate:"required,min=1,max=20"`
Remark string `json:"remark" validate:"min=0,max=100"`
Status uint `json:"status" validate:"oneof=1 2"`
Sort uint `json:"sort" validate:"gte=1,lte=999"`
}
// RoleListReq 列表结构体
type RoleListReq struct {
Name string `json:"name" form:"name"`
Keyword string `json:"keyword" form:"keyword"`
Status uint `json:"status" form:"status"`
PageNum int `json:"pageNum" form:"pageNum"`
PageSize int `json:"pageSize" form:"pageSize"`
}
// RoleUpdateReq 更新资源结构体
type RoleUpdateReq struct {
ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"required,min=1,max=20"`
Keyword string `json:"keyword" validate:"required,min=1,max=20"`
Remark string `json:"remark" validate:"min=0,max=100"`
Status uint `json:"status" validate:"oneof=1 2"`
Sort uint `json:"sort" validate:"gte=1,lte=999"`
}
// RoleDeleteReq 删除资源结构体
type RoleDeleteReq struct {
RoleIds []uint `json:"roleIds" validate:"required"`
}
// RoleGetTreeReq 获取资源树结构体
type RoleGetTreeReq struct {
}
// RoleGetMenuListReq 获取角色菜单列表结构体
type RoleGetMenuListReq struct {
RoleID uint `json:"roleId" form:"roleId" validate:"required"`
}
// RoleGetApiListReq 获取角色接口列表结构体
type RoleGetApiListReq struct {
RoleID uint `json:"roleId" form:"roleId" validate:"required"`
}
// RoleUpdateMenusReq 更新角色菜单结构体
type RoleUpdateMenusReq struct {
RoleID uint `json:"roleId" validate:"required"`
MenuIds []uint `json:"menuIds" validate:"required"`
}
// RoleUpdateApisReq 更新角色接口结构体
type RoleUpdateApisReq struct {
RoleID uint `json:"roleId" validate:"required"`
ApiIds []uint `json:"apiIds" validate:"required"`
}

72
svc/request/user_req.go Normal file
View File

@ -0,0 +1,72 @@
package request
// UserAddReq 创建资源结构体
type UserAddReq struct {
Username string `json:"username" validate:"required,min=2,max=20"`
Password string `json:"password"`
Nickname string `json:"nickname" validate:"required,min=0,max=20"`
GivenName string `json:"givenName" validate:"min=0,max=20"`
Mail string `json:"mail" validate:"required,min=0,max=20"`
JobNumber string `json:"jobNumber" validate:"required,min=0,max=20"`
PostalAddress string `json:"postalAddress" validate:"min=0,max=255"`
Departments string `json:"departments" validate:"min=0,max=255"`
Position string `json:"position" validate:"min=0,max=255"`
Mobile string `json:"mobile" validate:"required,checkMobile"`
Avatar string `json:"avatar"`
Introduction string `json:"introduction" validate:"min=0,max=255"`
Status uint `json:"status" validate:"oneof=1 2"`
RoleIds []uint `json:"roleIds" validate:"required"`
}
// UserUpdateReq 更新资源结构体
type UserUpdateReq struct {
ID uint `json:"id" validate:"required"`
Nickname string `json:"nickname" validate:"min=0,max=20"`
GivenName string `json:"givenName" validate:"min=0,max=20"`
Mail string `json:"mail" validate:"min=0,max=20"`
JobNumber string `json:"jobNumber" validate:"min=0,max=20"`
PostalAddress string `json:"postalAddress" validate:"min=0,max=255"`
Departments string `json:"departments" validate:"min=0,max=255"`
Position string `json:"position" validate:"min=0,max=255"`
Mobile string `json:"mobile" validate:"checkMobile"`
Avatar string `json:"avatar"`
Introduction string `json:"introduction" validate:"min=0,max=255"`
RoleIds []uint `json:"roleIds" validate:"required"`
}
// UserDeleteReq 批量删除资源结构体
type UserDeleteReq struct {
UserIds []uint `json:"userIds" validate:"required"`
}
// UserChangePwdReq 修改密码结构体
type UserChangePwdReq struct {
OldPassword string `json:"oldPassword" validate:"required"`
NewPassword string `json:"newPassword" validate:"required"`
}
// UserChangeUserStatusReq 修改用户状态结构体
type UserChangeUserStatusReq struct {
ID uint `json:"id" validate:"required"`
Status uint `json:"status" validate:"oneof=1 2"`
}
// UserGetUserInfoReq 获取用户信息结构体
type UserGetUserInfoReq struct {
}
// UserListReq 获取用户列表结构体
type UserListReq struct {
Username string `json:"username" form:"username"`
Mobile string `json:"mobile" form:"mobile" `
Nickname string `json:"nickname" form:"nickname"`
Status uint `json:"status" form:"status" `
PageNum int `json:"pageNum" form:"pageNum"`
PageSize int `json:"pageSize" form:"pageSize"`
}
// RegisterAndLoginReq 用户登录结构体
type RegisterAndLoginReq struct {
Username string `form:"username" json:"username" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}

15
svc/response/api_rsp.go Normal file
View File

@ -0,0 +1,15 @@
package response
import "github.com/eryajf-world/go-ldap-admin/model"
type ApiTreeRsp struct {
ID int `json:"ID"`
Remark string `json:"remark"`
Category string `json:"category"`
Children []*model.Api `json:"children"`
}
type ApiListRsp struct {
Total int64 `json:"total"`
Apis []model.Api `json:"apis"`
}

8
svc/response/base_rsp.go Normal file
View File

@ -0,0 +1,8 @@
package response
type DashboardList struct {
DataType string `json:"dataType"`
DataName string `json:"dataName"`
DataCount int64 `json:"dataCount"`
Icon string `json:"icon"`
}

View File

@ -0,0 +1,26 @@
package response
import (
"net/http"
"github.com/gin-gonic/gin"
)
// 返回前端
func Response(c *gin.Context, httpStatus int, code int, data gin.H, message string) {
c.JSON(httpStatus, gin.H{
"code": code,
"data": data,
"message": message,
})
}
// 返回前端-成功
func Success(c *gin.Context, data gin.H, message string) {
Response(c, http.StatusOK, 200, data, message)
}
// 返回前端-失败
func Fail(c *gin.Context, data gin.H, message string) {
Response(c, http.StatusBadRequest, 400, data, message)
}

25
svc/response/group_rsp.go Normal file
View File

@ -0,0 +1,25 @@
package response
import "github.com/eryajf-world/go-ldap-admin/model"
type GroupListRsp struct {
Total int64 `json:"total"`
Groups []model.Group `json:"groups"`
}
type Guser struct {
UserId int64 `json:"userId"`
UserName string `json:"userName"`
NickName string `json:"nickName"`
Mail string `json:"mail"`
JobNumber string `json:"jobNumber"`
Mobile string `json:"mobile"`
Introduction string `json:"introduction"`
}
type GroupUsers struct {
GroupId int64 `json:"groupId"`
GroupName string `json:"groupName"`
GroupRemark string `json:"groupRemark"`
UserList []Guser `json:"userList"`
}

8
svc/response/menu_rsp.go Normal file
View File

@ -0,0 +1,8 @@
package response
import "github.com/eryajf-world/go-ldap-admin/model"
type MenuListRsp struct {
Total int64 `json:"total"`
Menus []model.Menu `json:"menus"`
}

View File

@ -0,0 +1,8 @@
package response
import "github.com/eryajf-world/go-ldap-admin/model"
type LogListRsp struct {
Total int64 `json:"total"`
Logs []model.OperationLog `json:"logs"`
}

8
svc/response/role_rsp.go Normal file
View File

@ -0,0 +1,8 @@
package response
import "github.com/eryajf-world/go-ldap-admin/model"
type RoleListRsp struct {
Total int64 `json:"total"`
Roles []model.Role `json:"roles"`
}

8
svc/response/user_rsp.go Normal file
View File

@ -0,0 +1,8 @@
package response
import "github.com/eryajf-world/go-ldap-admin/model"
type UserListRsp struct {
Total int `json:"total"`
Users []model.User `json:"users"`
}

2
test/README.md Normal file
View File

@ -0,0 +1,2 @@
> 因为一些测试场景需要依赖配置的初始化,因此这里单独一个目录放类似的测试

1
test/config.yml Symbolic link
View File

@ -0,0 +1 @@
../config.yml

1
test/go-web-mini-priv.pem Symbolic link
View File

@ -0,0 +1 @@
../go-web-mini-priv.pem

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