:toda:
This commit is contained in:
commit
1f7f57ec9f
187
.gitignore
vendored
Normal file
187
.gitignore
vendored
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
### GoLand+all template
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# AWS User-specific
|
||||||
|
.idea/**/aws.xml
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/artifacts
|
||||||
|
# .idea/compiler.xml
|
||||||
|
# .idea/jarRepositories.xml
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
# *.iml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# SonarLint plugin
|
||||||
|
.idea/sonarlint/
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
### Go template
|
||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# 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/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
### GoLand template
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# AWS User-specific
|
||||||
|
.idea/**/aws.xml
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/artifacts
|
||||||
|
# .idea/compiler.xml
|
||||||
|
# .idea/jarRepositories.xml
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
# *.iml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# SonarLint plugin
|
||||||
|
.idea/sonarlint/
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
template/tmp/*
|
||||||
|
logs/*
|
||||||
|
app.yaml
|
||||||
|
*.db
|
||||||
|
.env
|
||||||
|
*.env
|
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
@ -0,0 +1 @@
|
|||||||
|
wireguard-ui
|
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="GoDfaErrorMayBeNotNil" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<functions>
|
||||||
|
<function importPath="github.com/golang-jwt/jwt/v5" name="ParseWithClaims" />
|
||||||
|
</functions>
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/wireguard-ui.iml" filepath="$PROJECT_DIR$/.idea/wireguard-ui.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
7
.idea/vcs.xml
generated
Normal file
7
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/web" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
8
.idea/watcherTasks.xml
generated
Normal file
8
.idea/watcherTasks.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectTasksOptions">
|
||||||
|
<enabled-global>
|
||||||
|
<option value="go fmt" />
|
||||||
|
</enabled-global>
|
||||||
|
</component>
|
||||||
|
</project>
|
9
.idea/wireguard-ui.iml
generated
Normal file
9
.idea/wireguard-ui.iml
generated
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
68
command/wireguard.go
Normal file
68
command/wireguard.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"wireguard-ui/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getConfigFileName
|
||||||
|
// @description: 获取服务端配置文件名称
|
||||||
|
// @return string
|
||||||
|
func getConfigFileName() string {
|
||||||
|
data, err := service.Setting().GetWGSetForConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("获取服务端配置失败: %v", err.Error())
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.ConfigFilePath != "" {
|
||||||
|
data.ConfigFilePath = strings.Split(data.ConfigFilePath, "/")[len(strings.Split(data.ConfigFilePath, "/"))-1] // 这里取到的是wg0.conf
|
||||||
|
data.ConfigFilePath = strings.Split(data.ConfigFilePath, ".conf")[0] // 这里取到的就是wg0
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.ConfigFilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestartWireguard
|
||||||
|
// @description: 是否重启
|
||||||
|
// @param isAsync // 是否异步执行
|
||||||
|
func RestartWireguard(isAsync bool) {
|
||||||
|
if isAsync {
|
||||||
|
go func() {
|
||||||
|
StopWireguard() // 停止
|
||||||
|
StartWireguard() // 启动
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
StopWireguard() // 停止
|
||||||
|
StartWireguard() // 启动
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopWireguard
|
||||||
|
// @description: 停止服务端
|
||||||
|
func StopWireguard() {
|
||||||
|
configFileName := getConfigFileName()
|
||||||
|
|
||||||
|
cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("wg-quick down %s", configFileName))
|
||||||
|
if err := cmd.Run(); err == nil {
|
||||||
|
log.Infof("停止wireguard[%s]服务端成功", configFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartWireguard
|
||||||
|
// @description: 启动服务端
|
||||||
|
func StartWireguard() {
|
||||||
|
configFileName := getConfigFileName()
|
||||||
|
|
||||||
|
cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("wg-quick up %s", configFileName))
|
||||||
|
if err := cmd.Run(); err == nil {
|
||||||
|
log.Infof("启动wireguard[%s]服务端成功", configFileName)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
57
component/captcha.go
Normal file
57
component/captcha.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package component
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"wireguard-ui/global/client"
|
||||||
|
"wireguard-ui/global/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Captcha struct{}
|
||||||
|
|
||||||
|
// Set
|
||||||
|
// @description: 验证码放入指定存储
|
||||||
|
// @receiver Captcha
|
||||||
|
// @param id
|
||||||
|
// @param value
|
||||||
|
// @return error
|
||||||
|
func (Captcha) Set(id string, value string) error {
|
||||||
|
return client.Redis.Set(context.Background(), fmt.Sprintf("%s:%s", constant.Captcha, id), value, 2*time.Minute).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get
|
||||||
|
// @description: 获取验证码信息
|
||||||
|
// @receiver Captcha
|
||||||
|
// @param id
|
||||||
|
// @param clear
|
||||||
|
// @return string
|
||||||
|
func (Captcha) Get(id string, clear bool) string {
|
||||||
|
val, err := client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.Captcha, id)).Result()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if clear {
|
||||||
|
client.Redis.Del(context.Background(), fmt.Sprintf("%s:%s", constant.Captcha, id))
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
// @description: 校验
|
||||||
|
// @receiver Captcha
|
||||||
|
// @param id
|
||||||
|
// @param answer
|
||||||
|
// @param clear
|
||||||
|
// @return bool
|
||||||
|
func (c Captcha) Verify(id, answer string, clear bool) bool {
|
||||||
|
if os.Getenv("GIN_MODE") != "release" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.ToUpper(answer) == strings.ToUpper(c.Get(id, clear))
|
||||||
|
}
|
105
component/jwt.go
Normal file
105
component/jwt.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package component
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"wireguard-ui/config"
|
||||||
|
"wireguard-ui/global/client"
|
||||||
|
"wireguard-ui/global/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
// jwt密钥
|
||||||
|
const secret = "JQo7L1RYa8ArFWuj0wC9PyM3VzmDIfXZ2d5tsTOBhNgviE64bnKqGpSckxUlHey6"
|
||||||
|
|
||||||
|
type JwtComponent struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWT
|
||||||
|
// @description: 初始化JWT组件
|
||||||
|
// @return JwtComponent
|
||||||
|
func JWT() JwtComponent {
|
||||||
|
return JwtComponent{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateToken
|
||||||
|
// @description: 生成token
|
||||||
|
// @receiver JwtComponent
|
||||||
|
// @param userId
|
||||||
|
// @return token
|
||||||
|
// @return expireTime
|
||||||
|
// @return err
|
||||||
|
func (JwtComponent) GenerateToken(userId string) (token string, expireTime *jwt.NumericDate, err error) {
|
||||||
|
timeNow := time.Now().Local()
|
||||||
|
expireTime = jwt.NewNumericDate(timeNow.Add(7 * time.Hour))
|
||||||
|
notBefore := jwt.NewNumericDate(timeNow)
|
||||||
|
issuedAt := jwt.NewNumericDate(timeNow)
|
||||||
|
claims := JwtComponent{
|
||||||
|
ID: userId,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
Issuer: config.Config.Http.Endpoint, // 颁发站点
|
||||||
|
Subject: "you can you up,no can no bb", // 发布主题
|
||||||
|
ExpiresAt: expireTime, // 过期时间
|
||||||
|
NotBefore: notBefore, // token不得早于该时间
|
||||||
|
IssuedAt: issuedAt, // token颁发时间
|
||||||
|
ID: strings.ReplaceAll(uuid.NewString(), "-", ""), // 该token的id
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
|
||||||
|
token, err = t.SignedString([]byte(secret))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("生成token失败: %v", err.Error())
|
||||||
|
return "", nil, errors.New("生成token失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Redis.Set(context.Background(), fmt.Sprintf("%s:%s", constant.UserToken, userId), token, 7*time.Hour)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseToken
|
||||||
|
// @description: 解析token
|
||||||
|
// @receiver JwtComponent
|
||||||
|
// @param token
|
||||||
|
// @return *JwtComponent
|
||||||
|
// @return error
|
||||||
|
func (JwtComponent) ParseToken(token string) (*JwtComponent, error) {
|
||||||
|
tokenStr := strings.Split(token, "Bearer ")[1]
|
||||||
|
|
||||||
|
t, err := jwt.ParseWithClaims(tokenStr, &JwtComponent{}, func(token *jwt.Token) (any, error) {
|
||||||
|
return []byte(secret), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if claims, ok := t.Claims.(*JwtComponent); ok && t.Valid {
|
||||||
|
userToken, err := client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.UserToken, claims.ID)).Result()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("缓存中用户[%s]的token查找失败: %v", claims.ID, err.Error())
|
||||||
|
return nil, errors.New("token不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
if userToken != tokenStr {
|
||||||
|
log.Errorf("token不一致")
|
||||||
|
return nil, errors.New("token错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
// @description: 退出登陆
|
||||||
|
// @receiver JwtComponent
|
||||||
|
// @param userId
|
||||||
|
// @return error
|
||||||
|
func (JwtComponent) Logout(userId string) error {
|
||||||
|
return client.Redis.Del(context.Background(), fmt.Sprintf("%s:%s", constant.UserToken, userId)).Err()
|
||||||
|
}
|
76
component/template.go
Normal file
76
component/template.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package component
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"html/template"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TemplateComponent struct{}
|
||||||
|
|
||||||
|
func Template() TemplateComponent {
|
||||||
|
return TemplateComponent{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
// @description: 渲染数据模板并生成对应文件
|
||||||
|
// @receiver t
|
||||||
|
// @param templateFilePath
|
||||||
|
// @param outFilePath
|
||||||
|
// @param data
|
||||||
|
// @return err
|
||||||
|
func (t TemplateComponent) Execute(templateFilePath, outFilePath string, data any) (err error) {
|
||||||
|
parseTemplate, err := t.ParseTemplate(templateFilePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("解析模板信息失败:%v", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = t.Render(parseTemplate, data, outFilePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("渲染模板失败: %v", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseTemplate
|
||||||
|
// @description: 解析模板
|
||||||
|
// @receiver t
|
||||||
|
// @param filepath
|
||||||
|
// @return t
|
||||||
|
// @return err
|
||||||
|
func (t TemplateComponent) ParseTemplate(filepath string) (tp *template.Template, err error) {
|
||||||
|
file, err := os.ReadFile(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tp, err = template.New("wg.conf").Parse(string(file))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render
|
||||||
|
// @description: 渲染模板
|
||||||
|
// @receiver t
|
||||||
|
// @param tp
|
||||||
|
// @param data
|
||||||
|
// @param filepath
|
||||||
|
// @return err
|
||||||
|
func (t TemplateComponent) Render(tp *template.Template, data any, filepath string) (err error) {
|
||||||
|
wg0Conf, err := os.Create(filepath)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("创建文件[%s]失败: %v", filepath, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err = wg0Conf.Close(); err != nil {
|
||||||
|
log.Errorf("关闭文件[%s]失败: %v", filepath, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return tp.Execute(wg0Conf, data)
|
||||||
|
}
|
92
component/validator.go
Normal file
92
component/validator.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package component
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
"github.com/go-playground/locales/en"
|
||||||
|
"github.com/go-playground/locales/zh"
|
||||||
|
ut "github.com/go-playground/universal-translator"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
zhTranslations "github.com/go-playground/validator/v10/translations/zh"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var validatorTrans ut.Translator
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
initValidatorTranslator()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(err error) string {
|
||||||
|
var errs validator.ValidationErrors
|
||||||
|
ok := errors.As(err, &errs)
|
||||||
|
if !ok {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
errMap := errs.Translate(validatorTrans)
|
||||||
|
|
||||||
|
var errMsg string
|
||||||
|
for _, v := range errMap {
|
||||||
|
errMsg = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return errMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
// initValidatorTranslator
|
||||||
|
// @description: 初始化翻译机
|
||||||
|
// @receiver vli
|
||||||
|
func initValidatorTranslator() {
|
||||||
|
//修改gin框架中的Validator属性,实现自定制
|
||||||
|
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
|
||||||
|
// 注册一个获取json tag的自定义方法
|
||||||
|
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
|
||||||
|
name := strings.SplitN(fld.Tag.Get("label"), ",", 2)[0]
|
||||||
|
if name == "-" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
})
|
||||||
|
|
||||||
|
zhT := zh.New() //中文翻译器
|
||||||
|
enT := en.New() //英文翻译器
|
||||||
|
|
||||||
|
// 第一个参数是备用(fallback)的语言环境
|
||||||
|
// 后面的参数是应该支持的语言环境(支持多个)
|
||||||
|
// uni := ut.New(zhT, zhT) 也是可以的
|
||||||
|
uni := ut.New(enT, zhT, enT)
|
||||||
|
|
||||||
|
// locale 通常取决于 http 请求头的 'Accept-Language'
|
||||||
|
var ok bool
|
||||||
|
// 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找
|
||||||
|
validatorTrans, ok = uni.GetTranslator("zh")
|
||||||
|
if !ok {
|
||||||
|
log.Errorf("获取翻译机失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := overrideTranslator(v, validatorTrans)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("覆盖原有翻译失败: %v", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrideTranslator
|
||||||
|
// @description: 覆盖原有翻译
|
||||||
|
// @param v
|
||||||
|
// @param translator
|
||||||
|
// @return error
|
||||||
|
func overrideTranslator(v *validator.Validate, translator ut.Translator) error {
|
||||||
|
err := zhTranslations.RegisterDefaultTranslations(v, translator)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
168
component/wireguard.go
Normal file
168
component/wireguard.go
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
package component
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
"gopkg.in/fsnotify/fsnotify.v1"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"wireguard-ui/command"
|
||||||
|
"wireguard-ui/config"
|
||||||
|
"wireguard-ui/global/client"
|
||||||
|
"wireguard-ui/model"
|
||||||
|
"wireguard-ui/template/render_data"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WireguardComponent struct{}
|
||||||
|
|
||||||
|
func Wireguard() WireguardComponent {
|
||||||
|
return WireguardComponent{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClients
|
||||||
|
// @description: 获取所有链接的客户端信息
|
||||||
|
// @receiver w
|
||||||
|
// @return peers
|
||||||
|
// @return err
|
||||||
|
func (w WireguardComponent) GetClients() (peers []wgtypes.Peer, err error) {
|
||||||
|
device, err := client.WireguardClient.Devices()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range device {
|
||||||
|
return v.Peers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClientByPublicKey
|
||||||
|
// @description: 根据公钥获取指定客户端信息
|
||||||
|
// @receiver w
|
||||||
|
// @return peer
|
||||||
|
// @return err
|
||||||
|
func (w WireguardComponent) GetClientByPublicKey(pk string) (peer *wgtypes.Peer, err error) {
|
||||||
|
peers, err := w.GetClients()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range peers {
|
||||||
|
if v.PublicKey.String() == pk {
|
||||||
|
return &v, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateClientFile
|
||||||
|
// @description: 生成客户端文件
|
||||||
|
// @receiver w
|
||||||
|
// @param clientInfo
|
||||||
|
// @param server
|
||||||
|
// @param setting
|
||||||
|
// @return filePath
|
||||||
|
// @return err
|
||||||
|
func (w WireguardComponent) GenerateClientFile(clientInfo *model.Client, server *render_data.Server, setting *render_data.ServerSetting) (filePath string, err error) {
|
||||||
|
var keys render_data.Keys
|
||||||
|
_ = json.Unmarshal([]byte(clientInfo.Keys), &keys)
|
||||||
|
|
||||||
|
var serverDNS []string
|
||||||
|
if clientInfo.UseServerDns == 1 {
|
||||||
|
serverDNS = setting.DnsServer
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理一下数据
|
||||||
|
execData := render_data.ClientConfig{
|
||||||
|
PrivateKey: keys.PrivateKey,
|
||||||
|
IpAllocation: clientInfo.IpAllocation,
|
||||||
|
MTU: setting.MTU,
|
||||||
|
DNS: strings.Join(serverDNS, ","),
|
||||||
|
PublicKey: server.PublicKey,
|
||||||
|
PresharedKey: keys.PresharedKey,
|
||||||
|
AllowedIPS: clientInfo.AllowedIps,
|
||||||
|
Endpoint: setting.EndpointAddress,
|
||||||
|
ListenPort: int(server.ListenPort),
|
||||||
|
PersistentKeepalive: setting.PersistentKeepalive,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不同环境下处理文件路径
|
||||||
|
var outPath = "/tmp/" + fmt.Sprintf("%s.conf", clientInfo.Name)
|
||||||
|
var templatePath = "./template/wg.client.conf"
|
||||||
|
if os.Getenv("GIN_MODE") != "release" {
|
||||||
|
outPath = "E:\\Workspace\\Go\\wireguard-dashboard\\template\\" + fmt.Sprintf("%s.conf", clientInfo.Name)
|
||||||
|
templatePath = "E:\\Workspace\\Go\\wireguard-dashboard\\template\\wg.client.conf"
|
||||||
|
}
|
||||||
|
|
||||||
|
err = Template().Execute(templatePath, outPath, execData)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("文件渲染失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
return outPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerControl
|
||||||
|
// @description: 服务端控制
|
||||||
|
// @receiver w
|
||||||
|
// @return error
|
||||||
|
func (w WireguardComponent) ServerControl(filePath string) {
|
||||||
|
switch config.Config.Wireguard.RestartMode {
|
||||||
|
case "NOW": // 立即执行
|
||||||
|
w.watchConfigFile(filePath)
|
||||||
|
case "DELAY": // 延迟执行
|
||||||
|
time.Sleep(time.Duration(config.Config.Wireguard.DelayTime) * time.Second)
|
||||||
|
w.watchConfigFile(filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// watchConfigFile
|
||||||
|
// @description: 监听并重新操作配置文件
|
||||||
|
// @receiver w
|
||||||
|
// @param filepath
|
||||||
|
func (w WireguardComponent) watchConfigFile(filepath string) {
|
||||||
|
go func() {
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("创建文件监控失败: %v", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer watcher.Close()
|
||||||
|
done := make(chan bool)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Op == fsnotify.Write {
|
||||||
|
command.RestartWireguard(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印监听事件
|
||||||
|
log.Infof("监听事件是:%s", event.String())
|
||||||
|
case _, ok := <-watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err = watcher.Add(filepath); err != nil {
|
||||||
|
log.Errorf("添加[%s]监听失败: %v", filepath, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
<-done
|
||||||
|
}()
|
||||||
|
}
|
12
config/config.go
Normal file
12
config/config.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
var Config *config
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Http *http `yaml:"http"`
|
||||||
|
Database *database `yaml:"database"`
|
||||||
|
Redis *redis `yaml:"redis"`
|
||||||
|
File *file `yaml:"file"`
|
||||||
|
Mail *mail `yaml:"email"`
|
||||||
|
Wireguard *wireguard `yaml:"wireguard"`
|
||||||
|
}
|
25
config/databse.go
Normal file
25
config/databse.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type database struct {
|
||||||
|
Driver string `yaml:"driver"`
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
User string `yaml:"user"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
Db string `yaml:"db"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d database) GetDSN() string {
|
||||||
|
switch d.Driver {
|
||||||
|
case "mysql":
|
||||||
|
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", d.User, d.Password, d.Host, d.Port, d.Db)
|
||||||
|
case "pgsql":
|
||||||
|
return fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai", d.Host, d.User, d.Password, d.Db, d.Port)
|
||||||
|
case "sqlite":
|
||||||
|
return fmt.Sprintf("%s.db", d.Db)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
10
config/file.go
Normal file
10
config/file.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type file struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
Endpoint string `yaml:"endpoint"`
|
||||||
|
AccessId string `yaml:"accessId"`
|
||||||
|
AccessSecret string `yaml:"accessSecret"`
|
||||||
|
BucketName string `yaml:"bucketName"`
|
||||||
|
}
|
6
config/http.go
Normal file
6
config/http.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type http struct {
|
||||||
|
Port uint `yaml:"port"`
|
||||||
|
Endpoint string `yaml:"endpoint"`
|
||||||
|
}
|
9
config/mail.go
Normal file
9
config/mail.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type mail struct {
|
||||||
|
Host string `json:"host" yaml:"host"`
|
||||||
|
Port int `json:"port" yaml:"port"`
|
||||||
|
User string `json:"user" yaml:"user"`
|
||||||
|
Password string `json:"password" yaml:"password"`
|
||||||
|
SkipTls bool `json:"skipTls" yaml:"skipTls"`
|
||||||
|
}
|
8
config/redis.go
Normal file
8
config/redis.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type redis struct {
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
Db int `yaml:"db"`
|
||||||
|
}
|
6
config/wireguard.go
Normal file
6
config/wireguard.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type wireguard struct {
|
||||||
|
RestartMode string `json:"restartMode"` // 重启模式 NOW - 立即重启 | DELAY - 延时 | HAND - 手动重启
|
||||||
|
DelayTime int64 `json:"delayTime"` // 延时重启的间隔(单位:秒)
|
||||||
|
}
|
17
global/client/client.go
Normal file
17
global/client/client.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/cowardmrx/go_aliyun_oss"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
WireguardClient *wgctrl.Client // wireguard客户端
|
||||||
|
DB *gorm.DB // 数据库客户端
|
||||||
|
Redis *redis.Client // redis客户端
|
||||||
|
HttpClient *resty.Client // http客户端
|
||||||
|
OSS *go_aliyun_oss.AliOssClient // oss客户端
|
||||||
|
)
|
22
global/constant/common_constant.go
Normal file
22
global/constant/common_constant.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
// Status 启用禁用
|
||||||
|
type Status int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Disabled Status = iota
|
||||||
|
Enabled
|
||||||
|
)
|
||||||
|
|
||||||
|
var StatusMap = map[Status]string{
|
||||||
|
Disabled: "禁用",
|
||||||
|
Enabled: "启用",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u Status) String() string {
|
||||||
|
if v, ok := StatusMap[u]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
return "未知类型"
|
||||||
|
}
|
6
global/constant/redis_key.go
Normal file
6
global/constant/redis_key.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
const (
|
||||||
|
Captcha = "captcha:"
|
||||||
|
UserToken = "token:"
|
||||||
|
)
|
22
global/constant/user_constant.go
Normal file
22
global/constant/user_constant.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
// UserType 用户类型
|
||||||
|
type UserType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
NormalAdmin UserType = iota
|
||||||
|
SuperAdmin
|
||||||
|
)
|
||||||
|
|
||||||
|
var UserTypeMap = map[UserType]string{
|
||||||
|
NormalAdmin: "普通管理员",
|
||||||
|
SuperAdmin: "超级管理员",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UserType) String() string {
|
||||||
|
if v, ok := UserTypeMap[u]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
return "未知类型"
|
||||||
|
}
|
7
global/constant/wireguard_constant.go
Normal file
7
global/constant/wireguard_constant.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultPostUpScript = "iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE"
|
||||||
|
DefaultPostDownScript = "iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE"
|
||||||
|
DefaultPreDownScript = ""
|
||||||
|
)
|
120
go.mod
Normal file
120
go.mod
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
module wireguard-ui
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
gitee.ltd/lxh/logger v1.0.15
|
||||||
|
github.com/cowardmrx/go_aliyun_oss v1.0.7
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0
|
||||||
|
github.com/gin-contrib/pprof v1.5.0
|
||||||
|
github.com/gin-gonic/gin v1.10.0
|
||||||
|
github.com/glebarez/sqlite v1.11.0
|
||||||
|
github.com/go-resty/resty/v2 v2.13.1
|
||||||
|
github.com/redis/go-redis/v9 v9.5.3
|
||||||
|
github.com/spf13/viper v1.19.0
|
||||||
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||||
|
gorm.io/driver/mysql v1.5.7
|
||||||
|
gorm.io/driver/postgres v1.5.9
|
||||||
|
gorm.io/gorm v1.25.10
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
|
github.com/caarlos0/env/v6 v6.10.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||||
|
github.com/go-kit/kit v0.12.0 // indirect
|
||||||
|
github.com/go-kit/log v0.2.1 // indirect
|
||||||
|
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.22.0 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect
|
||||||
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
|
github.com/jpillora/backoff v1.0.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/lixh00/loki-client-go v1.0.1 // indirect
|
||||||
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect
|
||||||
|
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||||
|
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||||
|
github.com/mdlayher/socket v0.4.1 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/mojocn/base64Captcha v1.3.6 // indirect
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
|
||||||
|
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.13.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.3.0 // indirect
|
||||||
|
github.com/prometheus/common v0.37.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.8.0 // indirect
|
||||||
|
github.com/prometheus/prometheus v1.8.2-0.20201028100903-3245b3267b24 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/spf13/afero v1.11.0 // indirect
|
||||||
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
go.uber.org/atomic v1.10.0 // indirect
|
||||||
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
|
go.uber.org/zap v1.23.0 // indirect
|
||||||
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
golang.org/x/crypto v0.23.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
|
golang.org/x/image v0.13.0 // indirect
|
||||||
|
golang.org/x/net v0.25.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.18.0 // indirect
|
||||||
|
golang.org/x/sync v0.6.0 // indirect
|
||||||
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
|
golang.org/x/text v0.15.0 // indirect
|
||||||
|
golang.org/x/time v0.5.0 // indirect
|
||||||
|
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b // indirect
|
||||||
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect
|
||||||
|
google.golang.org/grpc v1.62.1 // indirect
|
||||||
|
google.golang.org/protobuf v1.34.1 // indirect
|
||||||
|
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
modernc.org/libc v1.22.5 // indirect
|
||||||
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
|
modernc.org/memory v1.5.0 // indirect
|
||||||
|
modernc.org/sqlite v1.23.1 // indirect
|
||||||
|
)
|
20
http/api/api.go
Normal file
20
http/api/api.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"wireguard-ui/http/response"
|
||||||
|
"wireguard-ui/http/vo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetCurrentLoginUser
|
||||||
|
// @description: 获取当前登陆用户
|
||||||
|
// @param c
|
||||||
|
// @return *vo.User
|
||||||
|
func GetCurrentLoginUser(c *gin.Context) *vo.User {
|
||||||
|
if user, ok := c.Get("user"); ok {
|
||||||
|
return user.(*vo.User)
|
||||||
|
}
|
||||||
|
response.R(c).AuthorizationFailed("暂未登陆")
|
||||||
|
c.Abort()
|
||||||
|
return nil
|
||||||
|
}
|
239
http/api/client.go
Normal file
239
http/api/client.go
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"wireguard-ui/component"
|
||||||
|
"wireguard-ui/http/param"
|
||||||
|
"wireguard-ui/http/response"
|
||||||
|
"wireguard-ui/http/vo"
|
||||||
|
"wireguard-ui/model"
|
||||||
|
"wireguard-ui/service"
|
||||||
|
"wireguard-ui/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientApi struct{}
|
||||||
|
|
||||||
|
func Client() ClientApi {
|
||||||
|
return ClientApi{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save
|
||||||
|
// @description: 新增/编辑客户端
|
||||||
|
// @param c
|
||||||
|
func (ClientApi) Save(c *gin.Context) {
|
||||||
|
var p param.SaveClient
|
||||||
|
if err := c.ShouldBind(&p); err != nil {
|
||||||
|
response.R(c).Validator(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginUser *vo.User
|
||||||
|
if loginUser = GetCurrentLoginUser(c); c.IsAborted() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := service.Client().SaveClient(p, loginUser); err != nil {
|
||||||
|
response.R(c).FailedWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).OK()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
// @description: 删除客户端
|
||||||
|
// @receiver ClientApi
|
||||||
|
// @param c
|
||||||
|
func (ClientApi) Delete(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" || id == "undefined" {
|
||||||
|
response.R(c).FailedWithError("id不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := service.Client().Delete(id); err != nil {
|
||||||
|
response.R(c).FailedWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).OK()
|
||||||
|
}
|
||||||
|
|
||||||
|
// List
|
||||||
|
// @description: 客户端分页列表
|
||||||
|
// @receiver ClientApi
|
||||||
|
// @param c
|
||||||
|
func (ClientApi) List(c *gin.Context) {
|
||||||
|
var p param.ClientList
|
||||||
|
if err := c.ShouldBind(&p); err != nil {
|
||||||
|
response.R(c).Validator(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, total, err := service.Client().List(p)
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).Paginate(data, total, p.Current, p.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateKeys
|
||||||
|
// @description: 生成客户端密钥信息
|
||||||
|
// @receiver ClientApi
|
||||||
|
// @param c
|
||||||
|
func (ClientApi) GenerateKeys(c *gin.Context) {
|
||||||
|
// 为空,新增
|
||||||
|
privateKey, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError(fmt.Errorf("生成密钥失败: %v", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
publicKey := privateKey.PublicKey().String()
|
||||||
|
presharedKey, err := wgtypes.GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError(fmt.Errorf("生成密钥失败: %v", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keys := vo.Keys{
|
||||||
|
PrivateKey: privateKey.String(),
|
||||||
|
PublicKey: publicKey,
|
||||||
|
PresharedKey: presharedKey.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).OkWithData(keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateIP
|
||||||
|
// @description: 生成客户端IP
|
||||||
|
// @receiver ClientApi
|
||||||
|
// @param c
|
||||||
|
func (ClientApi) GenerateIP(c *gin.Context) {
|
||||||
|
// 获取一下服务端信息,因为IP分配需要根据服务端的IP制定
|
||||||
|
serverInfo, err := service.Setting().GetWGServerForConfig()
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError("获取服务端信息失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var assignIPS []string
|
||||||
|
// 只获取最新的一个
|
||||||
|
var clientInfo *model.Client
|
||||||
|
if err = service.Client().Order("created_at DESC").Take(&clientInfo).Error; err == nil {
|
||||||
|
// 遍历每一个ip是否可允许再分配
|
||||||
|
for _, ip := range strings.Split(clientInfo.IpAllocation, ",") {
|
||||||
|
if cast.ToInt64(utils.Network().GetIPSuffix(ip)) >= 255 {
|
||||||
|
log.Errorf("IP:[%s]已无法分配新IP", ip)
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
assignIPS = append(assignIPS, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ips := utils.Network().GenerateIPByIPS(serverInfo.Address, assignIPS...)
|
||||||
|
response.R(c).OkWithData(ips)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download
|
||||||
|
// @description: 下载客户端配置文件
|
||||||
|
// @receiver ClientApi
|
||||||
|
// @param c
|
||||||
|
func (ClientApi) Download(c *gin.Context) {
|
||||||
|
var id = c.Param("id")
|
||||||
|
if id == "" || id == "undefined" {
|
||||||
|
response.R(c).FailedWithError("id不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var downloadType = c.Param("type")
|
||||||
|
if downloadType == "" {
|
||||||
|
response.R(c).FailedWithError("参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := service.Client().GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError("获取客户端信息失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys vo.Keys
|
||||||
|
_ = json.Unmarshal([]byte(data.Keys), &keys)
|
||||||
|
|
||||||
|
globalSet, err := service.Setting().GetWGSetForConfig()
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError("获取失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverConf, err := service.Setting().GetWGServerForConfig()
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError("获取失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
outPath, err := component.Wireguard().GenerateClientFile(data, serverConf, globalSet)
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError(fmt.Errorf("生成失败: %v", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据不同下载类型执行不同逻辑
|
||||||
|
switch downloadType {
|
||||||
|
case "QRCODE": // 二维码
|
||||||
|
// 读取文件内容
|
||||||
|
fileContent, err := os.ReadFile(outPath)
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError("读取文件失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
png, err := utils.QRCode().GenerateQrCodeBase64(fileContent, 256)
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError("生成二维码失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.Remove(outPath); err != nil {
|
||||||
|
log.Errorf("删除临时文件失败: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).OkWithData(map[string]interface{}{
|
||||||
|
"qrCode": png,
|
||||||
|
})
|
||||||
|
case "FILE": // 文件
|
||||||
|
// 输出文件流
|
||||||
|
c.Header("Content-Type", "application/octet-stream")
|
||||||
|
c.Header("Content-Disposition", "attachment; filename="+outPath)
|
||||||
|
c.Header("Content-Transfer-Encoding", "binary")
|
||||||
|
c.Header("Connection", "keep-alive")
|
||||||
|
c.File(outPath)
|
||||||
|
if err = os.Remove(outPath); err != nil {
|
||||||
|
log.Errorf("删除临时文件失败: %s", err.Error())
|
||||||
|
}
|
||||||
|
case "EMAIL": // 邮件
|
||||||
|
if data.Email == "" {
|
||||||
|
response.R(c).FailedWithError("当前客户端并未配置通知邮箱!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = utils.Mail().SendMail(data.Email, fmt.Sprintf("客户端: %s", data.Name), "请查收附件", outPath)
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError("发送邮件失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.Remove(outPath); err != nil {
|
||||||
|
log.Errorf("删除临时文件失败: %s", err.Error())
|
||||||
|
}
|
||||||
|
response.R(c).OK()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
87
http/api/login.go
Normal file
87
http/api/login.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/mojocn/base64Captcha"
|
||||||
|
"wireguard-ui/component"
|
||||||
|
"wireguard-ui/http/param"
|
||||||
|
"wireguard-ui/http/response"
|
||||||
|
"wireguard-ui/service"
|
||||||
|
"wireguard-ui/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoginApi struct{}
|
||||||
|
|
||||||
|
func Login() LoginApi {
|
||||||
|
return LoginApi{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Captcha
|
||||||
|
// @description: 获取验证码
|
||||||
|
// @receiver login
|
||||||
|
// @param c
|
||||||
|
func (LoginApi) Captcha(c *gin.Context) {
|
||||||
|
math := base64Captcha.DriverMath{Height: 120, Width: 480, Fonts: []string{"ApothecaryFont.ttf", "3Dumb.ttf"}}
|
||||||
|
mathDriver := math.ConvertFonts()
|
||||||
|
|
||||||
|
capt := base64Captcha.NewCaptcha(mathDriver, component.Captcha{})
|
||||||
|
|
||||||
|
id, base64Str, _, err := capt.Generate()
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError(fmt.Errorf("生成验证码失败: %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).OkWithData(map[string]any{
|
||||||
|
"id": id,
|
||||||
|
"captcha": base64Str,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login
|
||||||
|
// @description: 登陆
|
||||||
|
// @receiver login
|
||||||
|
// @param c
|
||||||
|
func (LoginApi) Login(c *gin.Context) {
|
||||||
|
var p param.Login
|
||||||
|
if err := c.ShouldBind(&p); err != nil {
|
||||||
|
response.R(c).Validator(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证验证码是否正确
|
||||||
|
ok := component.Captcha{}.Verify(p.CaptchaId, p.CaptchaCode, true)
|
||||||
|
if !ok {
|
||||||
|
response.R(c).FailedWithError("验证码错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证码正确,查询用户信息
|
||||||
|
user, err := service.User().GetUserByAccount(p.Account)
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError("获取用户信息失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对比密码
|
||||||
|
if !utils.Password().ComparePassword(user.Password, p.Password) {
|
||||||
|
response.R(c).FailedWithError("密码错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成token
|
||||||
|
token, expireAt, err := component.JWT().GenerateToken(user.Id)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("用户[%s]生成token失败: %v", user.Account, err.Error())
|
||||||
|
response.R(c).FailedWithError("登陆失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).OkWithData(map[string]any{
|
||||||
|
"token": token,
|
||||||
|
"type": "Bearer",
|
||||||
|
"expireAt": expireAt,
|
||||||
|
})
|
||||||
|
}
|
252
http/api/user.go
Normal file
252
http/api/user.go
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"wireguard-ui/global/constant"
|
||||||
|
"wireguard-ui/http/param"
|
||||||
|
"wireguard-ui/http/response"
|
||||||
|
"wireguard-ui/model"
|
||||||
|
"wireguard-ui/service"
|
||||||
|
"wireguard-ui/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserApi struct{}
|
||||||
|
|
||||||
|
func User() UserApi {
|
||||||
|
return UserApi{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLoginUser
|
||||||
|
// @description: 获取登陆用户信息
|
||||||
|
// @receiver UserApi
|
||||||
|
// @param c
|
||||||
|
func (UserApi) GetLoginUser(c *gin.Context) {
|
||||||
|
loginUser, ok := c.Get("user")
|
||||||
|
if !ok {
|
||||||
|
response.R(c).AuthorizationFailed("未登陆")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).OkWithData(loginUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveUser
|
||||||
|
// @description: 新增/编辑用户信息
|
||||||
|
// @receiver UserApi
|
||||||
|
// @param c
|
||||||
|
func (UserApi) SaveUser(c *gin.Context) {
|
||||||
|
var p param.SaveUser
|
||||||
|
if err := c.ShouldBind(&p); err != nil {
|
||||||
|
response.R(c).Validator(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是新增用户判断该用户是否已经存在
|
||||||
|
if p.Id == "" {
|
||||||
|
if len(p.Account) < 2 || len(p.Account) > 20 {
|
||||||
|
response.R(c).FailedWithError(errors.New("账号长度在2-20位"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(p.Password) < 8 || len(p.Password) > 32 {
|
||||||
|
response.R(c).FailedWithError(errors.New("密码长度在8-32位"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
if err := service.User().Model(&model.User{}).Where("account = ?", p.Account).Count(&count).Error; err != nil {
|
||||||
|
response.R(c).FailedWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
response.R(c).FailedWithError(errors.New("该账号已存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userEnt := &model.User{
|
||||||
|
Base: model.Base{
|
||||||
|
Id: p.Id,
|
||||||
|
},
|
||||||
|
Account: p.Account,
|
||||||
|
Password: p.Password,
|
||||||
|
Nickname: p.Nickname,
|
||||||
|
Avatar: p.Avatar,
|
||||||
|
Contact: p.Contact,
|
||||||
|
IsAdmin: *p.IsAdmin,
|
||||||
|
Status: *p.Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := service.User().CreateUser(userEnt); err != nil {
|
||||||
|
response.R(c).FailedWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).OK()
|
||||||
|
}
|
||||||
|
|
||||||
|
// List
|
||||||
|
// @description: 用户列表
|
||||||
|
// @receiver UserApi
|
||||||
|
// @param c
|
||||||
|
func (UserApi) List(c *gin.Context) {
|
||||||
|
var p param.Page
|
||||||
|
if err := c.ShouldBind(&p); err != nil {
|
||||||
|
response.R(c).Validator(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, total, err := service.User().List(p)
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).Paginate(data, total, p.Current, p.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
// @description: 删除用户
|
||||||
|
// @receiver UserApi
|
||||||
|
// @param c
|
||||||
|
func (UserApi) Delete(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" || id == "undefined" {
|
||||||
|
response.R(c).FailedWithError("id不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是不是自己删除自己
|
||||||
|
if id == GetCurrentLoginUser(c).Id && c.IsAborted() {
|
||||||
|
response.R(c).FailedWithError("非法操作")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先查询一下
|
||||||
|
user, err := service.User().GetUserById(id)
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError("获取用户信息失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// admin用户不能被删除
|
||||||
|
if user.Account == "admin" {
|
||||||
|
response.R(c).FailedWithError("当前用户不能被删除")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = service.User().Delete(id); err != nil {
|
||||||
|
response.R(c).FailedWithError("删除用户失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).OK()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status
|
||||||
|
// @description: 设置用户状态
|
||||||
|
// @receiver UserApi
|
||||||
|
// @param c
|
||||||
|
func (UserApi) Status(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" || id == "undefined" {
|
||||||
|
response.R(c).FailedWithError("id不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是不是自己删除自己
|
||||||
|
if id == GetCurrentLoginUser(c).Id && c.IsAborted() {
|
||||||
|
response.R(c).FailedWithError("非法操作")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先查询一下
|
||||||
|
user, err := service.User().GetUserById(id)
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError("获取用户信息失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// admin用户不能被删除
|
||||||
|
if user.Account == "admin" {
|
||||||
|
response.R(c).FailedWithError("当前用户状态不可被变更")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var state = constant.Enabled
|
||||||
|
if user.Status == constant.Enabled {
|
||||||
|
state = constant.Disabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := service.User().Status(id, state); err != nil {
|
||||||
|
response.R(c).FailedWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).OK()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangePassword
|
||||||
|
// @description: 修改密码
|
||||||
|
// @receiver UserApi
|
||||||
|
// @param c
|
||||||
|
func (UserApi) ChangePassword(c *gin.Context) {
|
||||||
|
var p param.ChangePassword
|
||||||
|
if err := c.ShouldBind(&p); err != nil {
|
||||||
|
response.R(c).Validator(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := GetCurrentLoginUser(c)
|
||||||
|
if user == nil {
|
||||||
|
response.R(c).FailedWithError("用户信息错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断原密码是否对
|
||||||
|
if !utils.Password().ComparePassword(user.Password, p.OriginalPassword) {
|
||||||
|
response.R(c).FailedWithError("原密码错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改密码
|
||||||
|
if err := service.User().ChangePassword(user.Id, p.NewPassword); err != nil {
|
||||||
|
response.R(c).FailedWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).OK()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetPassword
|
||||||
|
// @description: 重置密码
|
||||||
|
// @receiver UserApi
|
||||||
|
// @param c
|
||||||
|
func (UserApi) ResetPassword(c *gin.Context) {
|
||||||
|
var id = c.Param("id")
|
||||||
|
if id == "" || id == "undefined" {
|
||||||
|
response.R(c).FailedWithError("id不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先查询一下
|
||||||
|
user, err := service.User().GetUserById(id)
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError("获取用户信息失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Status != constant.Enabled {
|
||||||
|
response.R(c).FailedWithError("当前用户不可重置密码")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改密码
|
||||||
|
if err := service.User().ChangePassword(user.Id, "admin123"); err != nil {
|
||||||
|
response.R(c).FailedWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).OK()
|
||||||
|
}
|
57
http/middleware/authorization.go
Normal file
57
http/middleware/authorization.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"strings"
|
||||||
|
"wireguard-ui/component"
|
||||||
|
"wireguard-ui/global/constant"
|
||||||
|
"wireguard-ui/http/response"
|
||||||
|
"wireguard-ui/service"
|
||||||
|
"wireguard-ui/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
// @description: 授权中间件
|
||||||
|
// @return gin.HandlerFunc
|
||||||
|
func Authorization() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
token := c.GetHeader("Authorization")
|
||||||
|
if token == "" || !strings.HasPrefix(token, "Bearer ") {
|
||||||
|
response.R(c).AuthorizationFailed("未登陆")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userClaims, err := component.JWT().ParseToken(token)
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).AuthorizationFailed("未登陆")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果token的颁发者与请求的站点不一致,则直接给它狗日的丢出去
|
||||||
|
if userClaims.Issuer != utils.WebSite().GetHost(c.Request.Header.Get("Referer")) {
|
||||||
|
response.R(c).AuthorizationFailed("未登陆")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询用户
|
||||||
|
user, err := service.User().GetUserById(userClaims.ID)
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError("用户不存在")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Status != constant.Enabled {
|
||||||
|
response.R(c).FailedWithError("用户状态异常,请联系管理员处理!")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将用户信息放入上下文
|
||||||
|
c.Set("user", &user)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
40
http/param/client.go
Normal file
40
http/param/client.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package param
|
||||||
|
|
||||||
|
import (
|
||||||
|
"wireguard-ui/global/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SaveClient
|
||||||
|
// @description: 新增/编辑客户端
|
||||||
|
type SaveClient struct {
|
||||||
|
Id string `json:"id" form:"id" label:"id" binding:"omitempty"` // id
|
||||||
|
Name string `json:"name" form:"name" label:"名称" binding:"required,min=1,max=64"` // 名称
|
||||||
|
Email string `json:"email" form:"email" label:"联系邮箱" binding:"omitempty"` // 联系邮箱
|
||||||
|
SubnetRange string `json:"subnetRange" form:"subnetRange" label:"子网范围" binding:"omitempty"` // 子网范围
|
||||||
|
IpAllocation []string `json:"ipAllocation" form:"ipAllocation" label:"客户端IP" binding:"required,dive"` // IP地址
|
||||||
|
AllowedIps []string `json:"allowedIps" form:"allowedIps" label:"allowedIps" binding:"omitempty,dive"` // 允许访问的IP段
|
||||||
|
ExtraAllowedIps []string `json:"extraAllowedIps" form:"extraAllowedIps" label:"extraAllowedIps" binding:"omitempty,dive"` // 其他允许访问的IP段
|
||||||
|
Endpoint string `json:"endpoint" form:"endpoint" label:"endpoint" binding:"omitempty"` // 服务端地址
|
||||||
|
UseServerDns *constant.Status `json:"useServerDns" form:"useServerDns" label:"useServerDns" binding:"required,oneof=0 1"` // 是否使用服务端DNS 1 - 是 | 0 - 否
|
||||||
|
Keys *Keys `json:"keys" form:"keys" label:"密钥信息" binding:"required"` // 密钥
|
||||||
|
Enabled *constant.Status `json:"enabled" form:"enabled" label:"状态" binding:"required,oneof=0 1"` // 状态 1 - 启用 | 0 - 禁用
|
||||||
|
OfflineMonitoring *constant.Status `json:"offlineMonitoring" form:"offlineMonitoring" label:"离线通知" binding:"required,oneof=0 1"` // 离线通知 1 - 启用 | 0 - 禁用
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys
|
||||||
|
// @description: 客户端密钥信息
|
||||||
|
type Keys struct {
|
||||||
|
PrivateKey string `json:"privateKey" form:"privateKey" label:"私钥" binding:"required"`
|
||||||
|
PublicKey string `json:"publicKey" form:"publicKey" label:"公钥" binding:"required"`
|
||||||
|
PresharedKey string `json:"presharedKey" form:"presharedKey" label:"共享密钥" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientList
|
||||||
|
// @description: 客户端列表
|
||||||
|
type ClientList struct {
|
||||||
|
Name string `json:"name" form:"name" label:"名称" binding:"omitempty"` // 客户端名称
|
||||||
|
Email string `json:"email" form:"email" label:"邮箱" binding:"omitempty,email"` // 联系邮箱
|
||||||
|
IpAllocation string `json:"ipAllocation" form:"ipAllocation" label:"IP范围段" binding:"omitempty"` // 客户端IP
|
||||||
|
Enabled *int `json:"enabled" form:"enabled" label:"状态" binding:"omitempty,oneof=0 1"` // 客户端状态
|
||||||
|
Page
|
||||||
|
}
|
10
http/param/login.go
Normal file
10
http/param/login.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package param
|
||||||
|
|
||||||
|
// Login
|
||||||
|
// @description: 登陆
|
||||||
|
type Login struct {
|
||||||
|
Account string `json:"account" form:"account" label:"账号" binding:"required,min=2,max=20"`
|
||||||
|
Password string `json:"password" form:"password" label:"密码" binding:"required,min=8,max=32"`
|
||||||
|
CaptchaId string `json:"captchaId" form:"captchaId" label:"验证码ID" binding:"required"`
|
||||||
|
CaptchaCode string `json:"captchaCode" form:"captchaCode" label:"验证码" binding:"required,len=4"`
|
||||||
|
}
|
6
http/param/request.go
Normal file
6
http/param/request.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package param
|
||||||
|
|
||||||
|
type Page struct {
|
||||||
|
Current int64 `json:"current" form:"current" label:"页码数" binding:"required"`
|
||||||
|
Size int64 `json:"size" form:"size" label:"每页数量" binging:"required"`
|
||||||
|
}
|
11
http/param/server.go
Normal file
11
http/param/server.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package param
|
||||||
|
|
||||||
|
type SaveServer struct {
|
||||||
|
IPScope []string `json:"ipScope" form:"IPScope" label:"IPScope" binding:"required"`
|
||||||
|
ListenPort uint64 `json:"listenPort" form:"listenPort" label:"listenPort" binding:"required"`
|
||||||
|
PrivateKey string `json:"privateKey" form:"privateKey" label:"privateKey" binding:"required"`
|
||||||
|
PublicKey string `json:"publicKey" form:"publicKey" label:"publicKey" binding:"required"`
|
||||||
|
PostUpScript string `json:"postUpScript,omitempty" form:"postUpScript" label:"postUpScript" binding:"omitempty"`
|
||||||
|
PreDownScript string `json:"preDownScript,omitempty" form:"preDownScript" label:"preDownScript" binding:"omitempty"`
|
||||||
|
PostDownScript string `json:"postDownScript,omitempty" form:"postDownScript" label:"postDownScript" binding:"omitempty"`
|
||||||
|
}
|
24
http/param/user.go
Normal file
24
http/param/user.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package param
|
||||||
|
|
||||||
|
import "wireguard-ui/global/constant"
|
||||||
|
|
||||||
|
// SaveUser
|
||||||
|
// @description: 新增/编辑用户信息
|
||||||
|
type SaveUser struct {
|
||||||
|
Id string `json:"id" form:"id" label:"id" binding:"omitempty"` // id
|
||||||
|
Account string `json:"account" form:"account" label:"账户号" binding:"required_without=Id"` // 账户号
|
||||||
|
Password string `json:"password" form:"password" label:"密码" binding:"required_without=Id"` // 密码
|
||||||
|
Nickname string `json:"nickname" form:"nickname" label:"昵称" binding:"required,min=2"` // 昵称
|
||||||
|
Avatar string `json:"avatar" form:"avatar" label:"头像" binding:"omitempty"` // 头像
|
||||||
|
Contact string `json:"contact" form:"contact" label:"联系方式" binding:"omitempty"` // 联系方式
|
||||||
|
IsAdmin *constant.UserType `json:"isAdmin" form:"isAdmin" label:"是否为管理员" binding:"required,oneof=0 1"` // 是否为管理员 0 - 否 | 1 - 是
|
||||||
|
Status *constant.Status `json:"status" form:"status" label:"状态" binding:"required,oneof=0 1"` // 用户状态 0 - 禁用 | 1 - 启用
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangePassword
|
||||||
|
// @description: 修改密码
|
||||||
|
type ChangePassword struct {
|
||||||
|
OriginalPassword string `json:"originalPassword" form:"originalPassword" label:"原密码" binding:"required,min=8,max=32"` // 原密码
|
||||||
|
NewPassword string `json:"newPassword" form:"newPassword" label:"新密码" binding:"required,min=8,max=32"` // 新密码
|
||||||
|
ConfirmPassword string `json:"confirmPassword" form:"confirmPassword" label:"确认密码" binding:"eqfield=NewPassword"` // 确认密码
|
||||||
|
}
|
109
http/response/response.go
Normal file
109
http/response/response.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
"wireguard-ui/component"
|
||||||
|
"wireguard-ui/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PageData[T any] struct {
|
||||||
|
Current int `json:"current"` // 当前页码
|
||||||
|
Size int `json:"size"` // 每页数量
|
||||||
|
Total int64 `json:"total"` // 总数
|
||||||
|
TotalPage int `json:"totalPage"` // 总页数
|
||||||
|
Records T `json:"records"` // 返回数据
|
||||||
|
}
|
||||||
|
|
||||||
|
type response struct {
|
||||||
|
c *gin.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func R(c *gin.Context) response {
|
||||||
|
return response{c}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r response) OK() {
|
||||||
|
r.c.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": http.StatusOK,
|
||||||
|
"message": "success",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r response) OkWithData(data any) {
|
||||||
|
r.c.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": http.StatusOK,
|
||||||
|
"message": "success",
|
||||||
|
"data": data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paginate
|
||||||
|
// @description: 页码数
|
||||||
|
// @receiver r
|
||||||
|
// @param data
|
||||||
|
// @param total
|
||||||
|
// @param current
|
||||||
|
// @param size
|
||||||
|
func (r response) Paginate(data any, total int64, current, size int64) {
|
||||||
|
// 处理一下页码、页数量
|
||||||
|
if current == -1 {
|
||||||
|
current = 1
|
||||||
|
size = total
|
||||||
|
}
|
||||||
|
// 计算总页码
|
||||||
|
totalPage := utils.Paginate().Generate(total, int(size))
|
||||||
|
// 返回结果
|
||||||
|
r.c.JSON(http.StatusOK, map[string]any{
|
||||||
|
"code": http.StatusOK,
|
||||||
|
"data": &PageData[any]{Current: int(current), Size: int(size), Total: total, TotalPage: totalPage, Records: data},
|
||||||
|
"message": "success",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r response) AuthorizationFailed(msg string) {
|
||||||
|
if msg == "" {
|
||||||
|
msg = "authorized failed"
|
||||||
|
}
|
||||||
|
r.c.JSON(http.StatusUnauthorized, gin.H{
|
||||||
|
"code": http.StatusUnauthorized,
|
||||||
|
"message": msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r response) Failed() {
|
||||||
|
r.c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": http.StatusBadRequest,
|
||||||
|
"message": "failed",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r response) FailedWithError(err any) {
|
||||||
|
var errStr string
|
||||||
|
switch err.(type) {
|
||||||
|
case error:
|
||||||
|
errStr = err.(error).Error()
|
||||||
|
case string:
|
||||||
|
errStr = err.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": http.StatusBadRequest,
|
||||||
|
"message": errStr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r response) Validator(err error) {
|
||||||
|
r.c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": http.StatusBadRequest,
|
||||||
|
"message": component.Error(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r response) Internal() {
|
||||||
|
r.c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"code": http.StatusInternalServerError,
|
||||||
|
"message": "server error",
|
||||||
|
})
|
||||||
|
}
|
23
http/router/client.go
Normal file
23
http/router/client.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"wireguard-ui/http/api"
|
||||||
|
"wireguard-ui/http/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClientApi
|
||||||
|
// @description: 登陆相关API
|
||||||
|
// @param r
|
||||||
|
func ClientApi(r *gin.RouterGroup) {
|
||||||
|
client := r.Group("client", middleware.Authorization())
|
||||||
|
{
|
||||||
|
client.POST("", api.Client().Save) // 新增/编辑客户端
|
||||||
|
client.DELETE("/:id", api.Client().Delete) // 删除客户端
|
||||||
|
client.GET("/list", api.Client().List) // 客户端列表
|
||||||
|
client.POST("/generate-keys", api.Client().GenerateKeys) // 生成客户端密钥
|
||||||
|
client.POST("/generate-ip", api.Client().GenerateIP) // 生成客户端IP
|
||||||
|
client.GET("/download/:id/:type", api.Client().Download) // 下载客户端配置文件
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
18
http/router/login.go
Normal file
18
http/router/login.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"wireguard-ui/http/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoginApi
|
||||||
|
// @description: 登陆相关API
|
||||||
|
// @param r
|
||||||
|
func LoginApi(r *gin.RouterGroup) {
|
||||||
|
login := r.Group("/login")
|
||||||
|
{
|
||||||
|
login.GET("/captcha", api.Login().Captcha) // 获取登陆验证码
|
||||||
|
login.POST("", api.Login().Login) // 登陆
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
35
http/router/root.go
Normal file
35
http/router/root.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Option func(engine *gin.RouterGroup)
|
||||||
|
|
||||||
|
var options []Option
|
||||||
|
|
||||||
|
func includeRouters(opts ...Option) {
|
||||||
|
options = append(options, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitRouter() *gin.Engine {
|
||||||
|
r := gin.New()
|
||||||
|
// 开启IP 追踪
|
||||||
|
r.ForwardedByClientIP = true
|
||||||
|
// 将请求打印至控制台
|
||||||
|
r.Use(gin.Logger())
|
||||||
|
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(r.Group("api"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func Rooters() {
|
||||||
|
includeRouters(
|
||||||
|
LoginApi,
|
||||||
|
UserApi,
|
||||||
|
ClientApi,
|
||||||
|
)
|
||||||
|
}
|
23
http/router/user.go
Normal file
23
http/router/user.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"wireguard-ui/http/api"
|
||||||
|
"wireguard-ui/http/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserApi
|
||||||
|
// @description: 用户相关API
|
||||||
|
// @param r
|
||||||
|
func UserApi(r *gin.RouterGroup) {
|
||||||
|
userApi := r.Group("user", middleware.Authorization())
|
||||||
|
{
|
||||||
|
userApi.GET("/info", api.User().GetLoginUser) // 获取当前登陆用户信息
|
||||||
|
userApi.POST("", api.User().SaveUser) // 新增/编辑用户
|
||||||
|
userApi.DELETE("/:id", api.User().Delete) // 删除用户
|
||||||
|
userApi.GET("/list", api.User().List) // 分页列表
|
||||||
|
userApi.PUT("/status/:id", api.User().Status) // 修改用户状态
|
||||||
|
userApi.PUT("/change-password", api.User().ChangePassword) // 修改用户密码
|
||||||
|
userApi.PUT("/reset-password/:id", api.User().ResetPassword) // 重置用户密码
|
||||||
|
}
|
||||||
|
}
|
33
http/vo/client.go
Normal file
33
http/vo/client.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package vo
|
||||||
|
|
||||||
|
import "wireguard-ui/model"
|
||||||
|
|
||||||
|
// ClientItem
|
||||||
|
// @description: 客户端信息
|
||||||
|
type ClientItem struct {
|
||||||
|
Id string `json:"id"` // id
|
||||||
|
Name string `json:"name"` // 名称
|
||||||
|
Email string `json:"email"` // 通知邮箱
|
||||||
|
//SubnetRange string `json:"subnetRange"` // 子网范围段
|
||||||
|
IpAllocation []string `json:"ipAllocation" gorm:"-"` // 分配的IP
|
||||||
|
IpAllocationStr string `json:"-" gorm:"ipAllocationStr"`
|
||||||
|
AllowedIps []string `json:"allowedIps" gorm:"-"` // 允许访问的IP
|
||||||
|
AllowedIpsStr string `json:"-" gorm:"allowedIpsStr"`
|
||||||
|
ExtraAllowedIps []string `json:"extraAllowedIps" gorm:"-"` // 其他允许访问的IP
|
||||||
|
ExtraAllowedIpsStr string `json:"-" gorm:"extraAllowedIpsStr"`
|
||||||
|
Endpoint string `json:"endpoint"` // 服务端点
|
||||||
|
UseServerDns int `json:"useServerDns"` // 是否使用服务端DNS
|
||||||
|
Keys *Keys `json:"keys" gorm:"-"` // 密钥等
|
||||||
|
KeysStr string `json:"-" gorm:"keys_str"`
|
||||||
|
CreateUser string `json:"createUser"` // 创建人
|
||||||
|
Enabled int `json:"enabled"` // 是否启用
|
||||||
|
OfflineMonitoring int `json:"offlineMonitoring"` // 离线通知
|
||||||
|
CreatedAt model.JsonTime `json:"createdAt"` // 创建时间
|
||||||
|
UpdatedAt model.JsonTime `json:"updatedAt"` // 更新时间
|
||||||
|
}
|
||||||
|
|
||||||
|
type Keys struct {
|
||||||
|
PrivateKey string `json:"privateKey"`
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
PresharedKey string `json:"presharedKey"`
|
||||||
|
}
|
28
http/vo/user.go
Normal file
28
http/vo/user.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package vo
|
||||||
|
|
||||||
|
import "wireguard-ui/global/constant"
|
||||||
|
|
||||||
|
// UserItem
|
||||||
|
// @description: 用户列表的数据
|
||||||
|
type UserItem struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Account string `json:"account"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
Contact string `json:"contact"`
|
||||||
|
IsAdmin constant.UserType `json:"isAdmin"`
|
||||||
|
Status constant.Status `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// User
|
||||||
|
// @description: 用户信息
|
||||||
|
type User struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Account string `json:"account"`
|
||||||
|
Password string `json:"-"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
Contact string `json:"contact"`
|
||||||
|
IsAdmin constant.UserType `json:"isAdmin"`
|
||||||
|
Status constant.Status `json:"status"`
|
||||||
|
}
|
147
initialize/init.go
Normal file
147
initialize/init.go
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
package initialize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gitee.ltd/lxh/logger"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"github.com/cowardmrx/go_aliyun_oss"
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
gl "gorm.io/gorm/logger"
|
||||||
|
"time"
|
||||||
|
"wireguard-ui/config"
|
||||||
|
"wireguard-ui/global/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Init
|
||||||
|
// @description: 初始化
|
||||||
|
func Init() {
|
||||||
|
initLogger() // 初始化日志
|
||||||
|
initConfig() // 读取配置文件
|
||||||
|
initWireguard() // 初始化wireguard客户端
|
||||||
|
initDatabase() // 初始化数据库
|
||||||
|
initRedis() // 初始化redis
|
||||||
|
initHttpClient() // 初始化http客户端
|
||||||
|
initOSS() // 初始化oss客户端链接
|
||||||
|
}
|
||||||
|
|
||||||
|
// initConfig
|
||||||
|
// @description: 初始化配置
|
||||||
|
func initConfig() {
|
||||||
|
vp := viper.New()
|
||||||
|
vp.SetConfigFile("app.yaml")
|
||||||
|
if err := vp.ReadInConfig(); err != nil {
|
||||||
|
log.Panicf("读取配置文件失败: %v", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := vp.Unmarshal(&config.Config); err != nil {
|
||||||
|
log.Panicf("解析配置文件失败: %v", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
vp.OnConfigChange(func(in fsnotify.Event) {
|
||||||
|
if err := vp.Unmarshal(&config.Config); err != nil {
|
||||||
|
log.Errorf("配置文件变动,读取失败: %v", err.Error())
|
||||||
|
} else {
|
||||||
|
initDatabase()
|
||||||
|
initRedis()
|
||||||
|
initOSS()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
vp.WatchConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitWireguard
|
||||||
|
// @description: 初始化wireguard客户端
|
||||||
|
func initWireguard() {
|
||||||
|
c, err := wgctrl.New()
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("初始化wireguard客户端失败: %v", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
client.WireguardClient = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// initDatabase
|
||||||
|
// @description: 初始化数据库
|
||||||
|
func initDatabase() {
|
||||||
|
// 不同驱动提供
|
||||||
|
var dbDialector gorm.Dialector
|
||||||
|
switch config.Config.Database.Driver {
|
||||||
|
case "mysql":
|
||||||
|
dbDialector = mysql.Open(config.Config.Database.GetDSN())
|
||||||
|
case "pgsql":
|
||||||
|
dbDialector = postgres.Open(config.Config.Database.GetDSN())
|
||||||
|
case "sqlite":
|
||||||
|
dbDialector = sqlite.Open(config.Config.Database.GetDSN())
|
||||||
|
}
|
||||||
|
|
||||||
|
logLevel := gl.Info
|
||||||
|
|
||||||
|
db, err := gorm.Open(dbDialector, &gorm.Config{
|
||||||
|
Logger: logger.NewGormLoggerWithConfig(gl.Config{
|
||||||
|
SlowThreshold: time.Second, // Slow SQL threshold
|
||||||
|
IgnoreRecordNotFoundError: false, // 忽略没找到结果的错误
|
||||||
|
LogLevel: logLevel, // Log level
|
||||||
|
Colorful: false, // Disable color
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("链接数据库[%s]失败:%v", config.Config.Database.Driver, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
client.DB = db
|
||||||
|
}
|
||||||
|
|
||||||
|
// initRedis
|
||||||
|
// @description: 初始化redis
|
||||||
|
func initRedis() {
|
||||||
|
c := redis.NewClient(&redis.Options{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", config.Config.Redis.Host, config.Config.Redis.Port),
|
||||||
|
Password: config.Config.Redis.Password,
|
||||||
|
DB: config.Config.Redis.Db,
|
||||||
|
})
|
||||||
|
|
||||||
|
client.Redis = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// initHttpClient
|
||||||
|
// @description: 初始化http客户端
|
||||||
|
func initHttpClient() {
|
||||||
|
client.HttpClient = resty.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
// initOSS
|
||||||
|
// @description: 初始化oss客户端
|
||||||
|
func initOSS() {
|
||||||
|
if config.Config.File.Type != "oss" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ossConfig := &go_aliyun_oss.AliOssConfig{
|
||||||
|
EndPoint: config.Config.File.Endpoint,
|
||||||
|
AccessKeyId: config.Config.File.AccessId,
|
||||||
|
AccessKeySecret: config.Config.File.AccessSecret,
|
||||||
|
BucketName: config.Config.File.BucketName,
|
||||||
|
OriginalFileName: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
ossClient := ossConfig.CreateOssConnect()
|
||||||
|
client.OSS = ossClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// initLogger
|
||||||
|
// @description: 初始化日志
|
||||||
|
func initLogger() {
|
||||||
|
logger.InitLogger(logger.LogConfig{
|
||||||
|
Mode: logger.Dev,
|
||||||
|
FileEnable: true,
|
||||||
|
})
|
||||||
|
}
|
42
main.go
Normal file
42
main.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"github.com/gin-contrib/pprof"
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
"wireguard-ui/config"
|
||||||
|
"wireguard-ui/http/router"
|
||||||
|
"wireguard-ui/initialize"
|
||||||
|
"wireguard-ui/script"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
initialize.Init()
|
||||||
|
if err := script.New().Do(); err != nil {
|
||||||
|
log.Errorf("执行脚本失败: %v", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
rand.New(rand.NewSource(time.Now().Local().UnixNano()))
|
||||||
|
router.Rooters()
|
||||||
|
handler := router.InitRouter()
|
||||||
|
|
||||||
|
if cast.ToBool(os.Getenv("ENABLED_PPROF")) {
|
||||||
|
pprof.Register(handler, "/monitoring")
|
||||||
|
}
|
||||||
|
|
||||||
|
httpServe := http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%d", config.Config.Http.Port),
|
||||||
|
Handler: handler,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := httpServe.ListenAndServe(); err != nil {
|
||||||
|
log.Panicf("启动http服务端失败: %v", err.Error())
|
||||||
|
}
|
||||||
|
}
|
67
model/base.go
Normal file
67
model/base.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Base
|
||||||
|
// @description: 数据模型基类
|
||||||
|
type Base struct {
|
||||||
|
Id string `json:"id" gorm:"primaryKey;type:varchar(36);not null;comment:'主键'"`
|
||||||
|
Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Base) BeforeCreate(*gorm.DB) (err error) {
|
||||||
|
if b.Id == "" {
|
||||||
|
b.Id = strings.ReplaceAll(uuid.NewString(), "-", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type JsonTime struct {
|
||||||
|
time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Timestamp struct {
|
||||||
|
CreatedAt JsonTime
|
||||||
|
UpdatedAt JsonTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jt JsonTime) MarshalJSON() ([]byte, error) {
|
||||||
|
if jt.IsZero() {
|
||||||
|
return []byte(`""`), nil
|
||||||
|
}
|
||||||
|
output := fmt.Sprintf("\"%s\"", jt.Format("2006-01-02 15:04:05"))
|
||||||
|
return []byte(output), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jt JsonTime) Value() (driver.Value, error) {
|
||||||
|
var zeroTime time.Time
|
||||||
|
if jt.Time.UnixNano() == zeroTime.UnixNano() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return jt.Time.Format("2006-01-02 15:04:05"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jt *JsonTime) Scan(v any) error {
|
||||||
|
value, ok := v.(time.Time)
|
||||||
|
if ok {
|
||||||
|
*jt = JsonTime{Time: value}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("can not convert %v to timestamp", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jt JsonTime) String() string {
|
||||||
|
if jt.IsZero() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
output := fmt.Sprintf("%s", jt.Format("2006-01-02 15:04:05"))
|
||||||
|
return output
|
||||||
|
}
|
18
model/other.go
Normal file
18
model/other.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type RequestLog struct {
|
||||||
|
Base
|
||||||
|
UserId string `json:"userId" gorm:"type:char(40);comment:'用户id'"`
|
||||||
|
ClientIP string `json:"clientIP" gorm:"type:varchar(60);not null;comment:'客户端IP'"`
|
||||||
|
Host string `json:"host" gorm:"type:varchar(255);not null;comment:'请求域名'"`
|
||||||
|
Method string `json:"method" gorm:"type:varchar(20);not null;comment:'请求方法[GET POST DELETE PUT PATCH]'"`
|
||||||
|
Uri string `json:"uri" gorm:"type:varchar(255);not null;comment:'请求path'"`
|
||||||
|
Header string `json:"header" gorm:"type:text;comment:'请求头'"`
|
||||||
|
Body string `json:"body" gorm:"type:text;comment:'请求体'"`
|
||||||
|
Form string `json:"form" gorm:"type:text;comment:'请求表单'"`
|
||||||
|
Query string `json:"query" gorm:"type:text;comment:'请求query'"`
|
||||||
|
UserAgent string `json:"userAgent" gorm:"type:text;comment:'ua信息'"`
|
||||||
|
Cost int64 `json:"cost" gorm:"type:int(10);comment:'请求耗时'"`
|
||||||
|
StatusCode int `json:"statusCode" gorm:"type:int(10);comment:'响应状态码'"`
|
||||||
|
Response string `json:"response" gorm:"type:text;comment:'返回数据'"`
|
||||||
|
}
|
12
model/setting.go
Normal file
12
model/setting.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type Setting struct {
|
||||||
|
Base
|
||||||
|
Code string `json:"code" gorm:"type:char(20);not null;index:idx_code; comment:'设定code'"`
|
||||||
|
Data string `json:"render_data" gorm:"type:text;not null; comment:'值'"`
|
||||||
|
Describe string `json:"describe" gorm:"type:text;default null;comment:'配置说明'"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Setting) TableName() string {
|
||||||
|
return "t_setting"
|
||||||
|
}
|
18
model/user.go
Normal file
18
model/user.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "wireguard-ui/global/constant"
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Base
|
||||||
|
Account string `json:"account" gorm:"type:varchar(50);not null;index:idx_account;comment: '登陆账号'"`
|
||||||
|
Password string `json:"password" gorm:"type:varchar(255);not null;comment: '密码'"`
|
||||||
|
Nickname string `json:"nickname" gorm:"type:varchar(50);not null;comment: '昵称'"`
|
||||||
|
Avatar string `json:"avatar" gorm:"type:varchar(255);not null;comment: '头像'"`
|
||||||
|
Contact string `json:"contact" gorm:"type:varchar(255);default null;comment: '联系方式(邮箱|电话)'"`
|
||||||
|
IsAdmin constant.UserType `json:"isAdmin" gorm:"type:tinyint(1);not null;comment: '是否为管理员(0 - 否 | 1 - 是)'"`
|
||||||
|
Status constant.Status `json:"status" gorm:"type:tinyint(1);not null;comment: '用户状态(0 - 否 | 1 - 是)'"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (User) TableName() string {
|
||||||
|
return "t_user"
|
||||||
|
}
|
24
model/wireguard.go
Normal file
24
model/wireguard.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "wireguard-ui/global/constant"
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
Base
|
||||||
|
Name string `json:"name" gorm:"type:varchar(100);not null;comment:'客户端名称'"`
|
||||||
|
Email string `json:"email" gorm:"type:varchar(100);default null;comment:'联系邮箱'"`
|
||||||
|
SubnetRange string `json:"subnetRange" gorm:"type:varchar(255);default null;comment:'子网范围'"`
|
||||||
|
IpAllocation string `json:"ipAllocation" gorm:"type:varchar(255);not null;comment:'客户端ip'"`
|
||||||
|
AllowedIps string `json:"allowedIps" gorm:"type:varchar(255);not null;comment:'允许访问的ip'"`
|
||||||
|
ExtraAllowedIps string `json:"extraAllowedIps" gorm:"type:varchar(255);default null;comment:'额外允许的ip范围'"`
|
||||||
|
Endpoint string `json:"endpoint" gorm:"type:varchar(255);default null;comment:'端点'"`
|
||||||
|
UseServerDns constant.Status `json:"useServerDns" gorm:"type:tinyint(1);default 1;comment:'是否使用服务端dns'"`
|
||||||
|
Keys string `json:"keys" gorm:"type:text;default null;comment:'公钥和密钥的json串'"`
|
||||||
|
UserId string `json:"userId" gorm:"type:char(36);not null;comment:'创建人id'"`
|
||||||
|
Enabled constant.Status `json:"enabled" gorm:"type:tinyint(1);default 1;comment:'状态(0 - 禁用 | 1 - 正常)'"`
|
||||||
|
OfflineMonitoring constant.Status `json:"offlineMonitoring" gorm:"tinyint(1);default 0;comment:'是否启用离线监听(0 - 禁用 | 1 - 启用)"`
|
||||||
|
User *User `json:"user" gorm:"foreignKey:UserId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Client) TableName() string {
|
||||||
|
return "t_client"
|
||||||
|
}
|
190
script/script.go
Normal file
190
script/script.go
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
package script
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
"os"
|
||||||
|
"wireguard-ui/component"
|
||||||
|
"wireguard-ui/global/client"
|
||||||
|
"wireguard-ui/global/constant"
|
||||||
|
"wireguard-ui/http/param"
|
||||||
|
"wireguard-ui/model"
|
||||||
|
"wireguard-ui/service"
|
||||||
|
"wireguard-ui/template/render_data"
|
||||||
|
"wireguard-ui/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type script struct{}
|
||||||
|
|
||||||
|
func New() script {
|
||||||
|
return script{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s script) Do() error {
|
||||||
|
if err := s.migrate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.createSuperAdmin(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.initServer(); err != nil {
|
||||||
|
log.Errorf("初始化wg服务端失败: %v", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrate
|
||||||
|
// @description: 生成数据库
|
||||||
|
// @receiver s
|
||||||
|
// @return error
|
||||||
|
func (s script) migrate() error {
|
||||||
|
var ent = []any{
|
||||||
|
new(model.User),
|
||||||
|
new(model.Client),
|
||||||
|
new(model.Setting),
|
||||||
|
new(model.RequestLog),
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.DB.AutoMigrate(ent...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createSuperAdmin
|
||||||
|
// @description: 创建超级管理员
|
||||||
|
// @receiver s
|
||||||
|
// @return error
|
||||||
|
func (s script) createSuperAdmin() error {
|
||||||
|
var count int64
|
||||||
|
if err := client.DB.Model(&model.User{}).
|
||||||
|
Where("account = ?", "admin").
|
||||||
|
Where("is_admin = ?", 1).Count(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有超管就创建一个
|
||||||
|
if count > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成一下头像
|
||||||
|
avatarPath, err := utils.Avatar().GenerateAvatar(true)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("生成头像失败: %v", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return service.User().CreateUser(&model.User{
|
||||||
|
Avatar: avatarPath,
|
||||||
|
Nickname: "超级管理员",
|
||||||
|
Account: "admin",
|
||||||
|
Contact: "",
|
||||||
|
Password: "admin123",
|
||||||
|
IsAdmin: constant.SuperAdmin,
|
||||||
|
Status: constant.Enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// initServer
|
||||||
|
// @description: 初始化wg的一些配置
|
||||||
|
// @receiver s
|
||||||
|
// @return error
|
||||||
|
func (s script) initServer() error {
|
||||||
|
var count int64
|
||||||
|
if err := client.DB.Model(&model.Setting{}).Where("code = ?", "WG_SERVER").Count(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化服务端的全局配置
|
||||||
|
var data = map[string]any{
|
||||||
|
"endpointAddress": utils.Network().GetHostPublicIP(),
|
||||||
|
"dnsServer": []string{"10.25.8.1"},
|
||||||
|
"MTU": 1450,
|
||||||
|
"persistentKeepalive": 15,
|
||||||
|
"firewallMark": "0xca6c",
|
||||||
|
"table": "",
|
||||||
|
"configFilePath": "/etc/wireguard/wg0.conf",
|
||||||
|
}
|
||||||
|
dataJ, _ := json.Marshal(data)
|
||||||
|
|
||||||
|
globalSet := &model.Setting{
|
||||||
|
Code: "WG_SETTING",
|
||||||
|
Data: string(dataJ),
|
||||||
|
Describe: "服务端全局配置",
|
||||||
|
}
|
||||||
|
if err := service.Setting().SetData(globalSet); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成密钥
|
||||||
|
privateKey, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("生成密钥失败: %v", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据密钥生成公钥
|
||||||
|
publicKey := privateKey.PublicKey()
|
||||||
|
serverEnt := ¶m.SaveServer{
|
||||||
|
IPScope: []string{"10.25.8.1/24"},
|
||||||
|
ListenPort: 51820,
|
||||||
|
PrivateKey: privateKey.String(),
|
||||||
|
PublicKey: publicKey.String(),
|
||||||
|
PostUpScript: constant.DefaultPostUpScript,
|
||||||
|
PreDownScript: constant.DefaultPreDownScript,
|
||||||
|
PostDownScript: constant.DefaultPostDownScript,
|
||||||
|
}
|
||||||
|
|
||||||
|
serverJ, _ := json.Marshal(serverEnt)
|
||||||
|
serverSet := &model.Setting{
|
||||||
|
Code: "WG_SERVER",
|
||||||
|
Data: string(serverJ),
|
||||||
|
Describe: "服务端配置",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有服务端,开始初始化
|
||||||
|
if err = service.Setting().SetData(serverSet); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理一下要渲染到配置文件上的数据
|
||||||
|
serverConfig := render_data.Server{
|
||||||
|
Address: serverEnt.IPScope,
|
||||||
|
ListenPort: serverEnt.ListenPort,
|
||||||
|
PrivateKey: serverEnt.PrivateKey,
|
||||||
|
MTU: cast.ToInt(data["MTU"]),
|
||||||
|
PostUp: serverEnt.PostUpScript,
|
||||||
|
PreDown: serverEnt.PreDownScript,
|
||||||
|
PostDown: serverEnt.PostDownScript,
|
||||||
|
Table: cast.ToString(data["table"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
execData := map[string]any{
|
||||||
|
"Server": serverConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
var templatePath, outFilePath string
|
||||||
|
if os.Getenv("GIN_MODE") != "release" {
|
||||||
|
templatePath = "E:\\Workspace\\Go\\wireguard-ui\\template\\conf\\wg.conf"
|
||||||
|
outFilePath = "E:\\Workspace\\Go\\wireguard-ui\\template\\tmp\\wg0.conf"
|
||||||
|
} else {
|
||||||
|
templatePath = "./template/wg.conf"
|
||||||
|
outFilePath = cast.ToString(data["configFilePath"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先渲染模板
|
||||||
|
if err = component.Template().Execute(templatePath, outFilePath, execData); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模板渲染成功,开始执行服务端控制
|
||||||
|
component.Wireguard().ServerControl(outFilePath)
|
||||||
|
return nil
|
||||||
|
}
|
25
service/base.go
Normal file
25
service/base.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
|
func Paginate(current, size int64) func(db *gorm.DB) *gorm.DB {
|
||||||
|
// 如果页码是-1,就不分页
|
||||||
|
if current == -1 {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 分页
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
if current == 0 {
|
||||||
|
current = 1
|
||||||
|
}
|
||||||
|
if size < 1 {
|
||||||
|
size = 10
|
||||||
|
}
|
||||||
|
// 计算偏移量
|
||||||
|
offset := (current - 1) * size
|
||||||
|
// 返回组装结果
|
||||||
|
return db.Offset(int(offset)).Limit(int(size))
|
||||||
|
}
|
||||||
|
}
|
171
service/client.go
Normal file
171
service/client.go
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"strings"
|
||||||
|
gdb "wireguard-ui/global/client"
|
||||||
|
"wireguard-ui/http/param"
|
||||||
|
"wireguard-ui/http/vo"
|
||||||
|
"wireguard-ui/model"
|
||||||
|
"wireguard-ui/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type client struct {
|
||||||
|
*gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func Client() client {
|
||||||
|
return client{
|
||||||
|
gdb.DB,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveClient
|
||||||
|
// @description: 新增/编辑客户端
|
||||||
|
// @receiver s
|
||||||
|
// @param p
|
||||||
|
// @param loginUser
|
||||||
|
// @return error
|
||||||
|
func (s client) SaveClient(p param.SaveClient, loginUser *vo.User) error {
|
||||||
|
serverConf, err := Setting().GetWGServerForConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对客户端IP做格式校验
|
||||||
|
for _, cip := range p.IpAllocation {
|
||||||
|
if !utils.Network().IPContains(serverConf.Address, cip) {
|
||||||
|
return fmt.Errorf("客户端IP[%s]不符合定义", cip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理一下endpoint
|
||||||
|
if p.Endpoint == "" {
|
||||||
|
globalConf, err := Setting().GetWGSetForConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Endpoint = fmt.Sprintf("%s:%d", globalConf.EndpointAddress, serverConf.ListenPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys, _ := json.Marshal(p.Keys)
|
||||||
|
|
||||||
|
ent := &model.Client{
|
||||||
|
Base: model.Base{
|
||||||
|
Id: p.Id,
|
||||||
|
},
|
||||||
|
Name: p.Name,
|
||||||
|
Email: p.Email,
|
||||||
|
SubnetRange: p.SubnetRange,
|
||||||
|
IpAllocation: strings.Join(p.IpAllocation, ","),
|
||||||
|
AllowedIps: strings.Join(p.AllowedIps, ","),
|
||||||
|
ExtraAllowedIps: strings.Join(p.ExtraAllowedIps, ","),
|
||||||
|
Endpoint: p.Endpoint,
|
||||||
|
UseServerDns: *p.UseServerDns,
|
||||||
|
Keys: string(keys),
|
||||||
|
UserId: loginUser.Id,
|
||||||
|
Enabled: *p.Enabled,
|
||||||
|
OfflineMonitoring: *p.OfflineMonitoring,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
if p.Id != "" {
|
||||||
|
return s.Model(&model.Client{}).Select("id", "name", "email", "subnet_range",
|
||||||
|
"ip_allocation", "allowed_ips",
|
||||||
|
"extra_allowed_ips", "endpoint",
|
||||||
|
"use_server_dns", "keys", "user_id", "enabled",
|
||||||
|
"offline_monitoring").
|
||||||
|
Where("id = ?", ent.Id).Updates(&ent).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是新增,判断这个客户端IP是否已经存在过了
|
||||||
|
var count int64
|
||||||
|
if err := s.Model(&model.Client{}).Where("ip_allocation = ?", strings.Join(p.IpAllocation, ",")).Count(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
return errors.New("当前IP客户端已经存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增
|
||||||
|
return s.Model(&model.Client{}).Create(&ent).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
// @description: 删除
|
||||||
|
// @receiver s
|
||||||
|
// @param id
|
||||||
|
// @return error
|
||||||
|
func (s client) Delete(id string) error {
|
||||||
|
return s.Model(&model.Client{}).Where("id = ?", id).Delete(&model.Client{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// List
|
||||||
|
// @description: 客户端分页列表
|
||||||
|
// @receiver s
|
||||||
|
// @param p
|
||||||
|
// @return data
|
||||||
|
// @return total
|
||||||
|
// @return err
|
||||||
|
func (s client) List(p param.ClientList) (data []vo.ClientItem, total int64, err error) {
|
||||||
|
sel := s.Table("t_client as tc").Scopes(Paginate(p.Current, p.Size)).
|
||||||
|
Joins("LEFT JOIN t_user as tu ON tu.id = tc.user_id").
|
||||||
|
Select("tc.id,tc.name,tc.email,tc.ip_allocation as ip_allocation_str,"+
|
||||||
|
"tc.allowed_ips as allowed_ips_str,tc.extra_allowed_ips as extra_allowed_ips_str,"+
|
||||||
|
"tc.endpoint,tc.use_server_dns,tc.keys as keys_str,tu.nickname as create_user,"+
|
||||||
|
"tc.enabled,tc.offline_monitoring,"+
|
||||||
|
"tc.created_at", "tc.updated_at")
|
||||||
|
if p.Enabled != nil {
|
||||||
|
sel.Where("tc.enabled = ?", *p.Enabled)
|
||||||
|
}
|
||||||
|
if p.Name != "" {
|
||||||
|
sel.Where("tc.name like ?", "%"+p.Name+"%")
|
||||||
|
}
|
||||||
|
if p.Email != "" {
|
||||||
|
sel.Where("tc.email like ?", "%"+p.Email+"%")
|
||||||
|
}
|
||||||
|
if p.IpAllocation != "" {
|
||||||
|
sel.Where("tc.ip_allocation like ?", "%"+p.IpAllocation+"%")
|
||||||
|
}
|
||||||
|
err = sel.Order("tc.created_at DESC").Find(&data).Limit(-1).Offset(-1).Count(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v := range data {
|
||||||
|
if v.KeysStr != "" {
|
||||||
|
_ = json.Unmarshal([]byte(v.KeysStr), &data[i].Keys)
|
||||||
|
}
|
||||||
|
if v.IpAllocationStr != "" {
|
||||||
|
data[i].IpAllocation = strings.Split(v.IpAllocationStr, ",")
|
||||||
|
}
|
||||||
|
if v.AllowedIpsStr != "" {
|
||||||
|
data[i].AllowedIps = strings.Split(v.AllowedIpsStr, ",")
|
||||||
|
} else {
|
||||||
|
data[i].AllowedIps = []string{}
|
||||||
|
}
|
||||||
|
if v.ExtraAllowedIpsStr != "" {
|
||||||
|
data[i].ExtraAllowedIps = strings.Split(v.ExtraAllowedIpsStr, ",")
|
||||||
|
} else {
|
||||||
|
data[i].ExtraAllowedIps = []string{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID
|
||||||
|
// @description: 通过ID获取客户端
|
||||||
|
// @receiver s
|
||||||
|
// @param id
|
||||||
|
// @return data
|
||||||
|
// @return err
|
||||||
|
func (s client) GetByID(id string) (data *model.Client, err error) {
|
||||||
|
err = s.Model(&model.Client{}).Where("id = ?", id).Take(&data).Error
|
||||||
|
return
|
||||||
|
}
|
74
service/setting.go
Normal file
74
service/setting.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
gdb "wireguard-ui/global/client"
|
||||||
|
"wireguard-ui/model"
|
||||||
|
"wireguard-ui/template/render_data"
|
||||||
|
)
|
||||||
|
|
||||||
|
type setting struct{ *gorm.DB }
|
||||||
|
|
||||||
|
func Setting() setting {
|
||||||
|
return setting{gdb.DB}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s setting) SetData(data *model.Setting) error {
|
||||||
|
// 判断code是否已经存在
|
||||||
|
var count int64
|
||||||
|
if err := s.Model(&model.Setting{}).Where("code = ?", data.Code).Count(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存在就更新,反之新增
|
||||||
|
if count > 0 {
|
||||||
|
return s.Save(&data).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Create(&data).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWGSetForConfig
|
||||||
|
// @description: 获取全局配置
|
||||||
|
// @receiver s
|
||||||
|
// @return data
|
||||||
|
// @return err
|
||||||
|
func (s setting) GetWGSetForConfig() (data *render_data.ServerSetting, err error) {
|
||||||
|
var rs *model.Setting
|
||||||
|
if err = s.Model(&model.Setting{}).Where("code = ?", "WG_SETTING").Take(&rs).Error; err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal([]byte(rs.Data), &data); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWGServerForConfig
|
||||||
|
// @description: 获取wireguard服务端配置为了渲染数据
|
||||||
|
// @receiver s
|
||||||
|
// @return render_data
|
||||||
|
// @return err
|
||||||
|
func (s setting) GetWGServerForConfig() (data *render_data.Server, err error) {
|
||||||
|
var rs *model.Setting
|
||||||
|
if err = s.Model(&model.Setting{}).Where("code = ?", "WG_SERVER").Take(&rs).Error; err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal([]byte(rs.Data), &data); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取一下全局的服务配置
|
||||||
|
gs, err := s.GetWGSetForConfig()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.MTU = gs.MTU
|
||||||
|
data.Table = gs.Table
|
||||||
|
return
|
||||||
|
}
|
126
service/user.go
Normal file
126
service/user.go
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/gorm"
|
||||||
|
gdb "wireguard-ui/global/client"
|
||||||
|
"wireguard-ui/global/constant"
|
||||||
|
"wireguard-ui/http/param"
|
||||||
|
"wireguard-ui/http/vo"
|
||||||
|
"wireguard-ui/model"
|
||||||
|
"wireguard-ui/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type user struct {
|
||||||
|
*gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func User() user {
|
||||||
|
return user{
|
||||||
|
gdb.DB,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser
|
||||||
|
// @description: 创建用户
|
||||||
|
// @receiver s
|
||||||
|
// @param user
|
||||||
|
// @return err
|
||||||
|
func (s user) CreateUser(user *model.User) (err error) {
|
||||||
|
// 更新
|
||||||
|
if user.Id != "" {
|
||||||
|
updates := map[string]any{
|
||||||
|
"nickname": user.Nickname,
|
||||||
|
"avatar": user.Avatar,
|
||||||
|
"contact": user.Contact,
|
||||||
|
"is_admin": user.IsAdmin,
|
||||||
|
"status": user.Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Model(&model.User{}).Where("id = ?", user.Id).Updates(&updates).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultPassword := utils.Password().GenerateHashPassword("admin123")
|
||||||
|
if user.Password == "" { // 没有密码给一个默认密码
|
||||||
|
user.Password = defaultPassword
|
||||||
|
} else {
|
||||||
|
user.Password = utils.Password().GenerateHashPassword(user.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有头像就生成一个头像
|
||||||
|
if user.Avatar == "" {
|
||||||
|
user.Avatar, _ = utils.Avatar().GenerateAvatar(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建
|
||||||
|
return s.Create(&user).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
// @description: 删除用户
|
||||||
|
// @receiver s
|
||||||
|
// @param id
|
||||||
|
// @return error
|
||||||
|
func (s user) Delete(id string) error {
|
||||||
|
return s.Model(&model.User{}).Where("id = ?", id).Delete(&model.User{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// List
|
||||||
|
// @description: 用户列表
|
||||||
|
// @receiver s
|
||||||
|
// @param p
|
||||||
|
// @return data
|
||||||
|
// @return count
|
||||||
|
// @return err
|
||||||
|
func (s user) List(p param.Page) (data []vo.UserItem, count int64, err error) {
|
||||||
|
if err = s.Model(&model.User{}).
|
||||||
|
Scopes(Paginate(p.Current, p.Size)).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&data).
|
||||||
|
Offset(-1).Limit(-1).
|
||||||
|
Count(&count).Error; err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByAccount
|
||||||
|
// @description: 根据账户获取用户信息
|
||||||
|
// @receiver s
|
||||||
|
// @param id
|
||||||
|
// @return data
|
||||||
|
// @return err
|
||||||
|
func (s user) GetUserByAccount(account string) (data vo.User, err error) {
|
||||||
|
err = s.Model(&model.User{}).Where("account = ?", account).Take(&data).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserById
|
||||||
|
// @description: 根据id获取用户信息
|
||||||
|
// @receiver s
|
||||||
|
// @param id
|
||||||
|
// @return data
|
||||||
|
// @return err
|
||||||
|
func (s user) GetUserById(id string) (data vo.User, err error) {
|
||||||
|
err = s.Model(&model.User{}).Where("id = ?", id).Take(&data).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status
|
||||||
|
// @description: 修改用户状态
|
||||||
|
// @receiver s
|
||||||
|
// @param id
|
||||||
|
// @return error
|
||||||
|
func (s user) Status(id string, state constant.Status) error {
|
||||||
|
return s.Model(&model.User{}).Where("id = ?", id).Update("status", state).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangePassword
|
||||||
|
// @description: 修改密码
|
||||||
|
// @receiver s
|
||||||
|
// @param id
|
||||||
|
// @param password
|
||||||
|
// @return error
|
||||||
|
func (s user) ChangePassword(id, password string) error {
|
||||||
|
return s.Model(&model.User{}).Where("id = ?", id).Update("password", utils.Password().GenerateHashPassword(password)).Error
|
||||||
|
}
|
13
template/conf/wg.client.conf
Normal file
13
template/conf/wg.client.conf
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[Interface]
|
||||||
|
PrivateKey = {{ .PrivateKey|html }}
|
||||||
|
Address = {{ .IpAllocation|html }}
|
||||||
|
{{ if .DNS }}DNS = {{ .DNS|html }}
|
||||||
|
MTU = {{ .MTU }}
|
||||||
|
{{ else }}MTU = {{ .MTU }}{{ end }}
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = {{ .PublicKey|html }}
|
||||||
|
PresharedKey = {{ .PresharedKey|html }}
|
||||||
|
AllowedIPs = {{ .AllowedIPS|html }}
|
||||||
|
Endpoint = {{ .Endpoint|html }}:{{ .ListenPort|html }}
|
||||||
|
PersistentKeepalive = {{ .PersistentKeepalive|html }}
|
24
template/conf/wg.conf
Normal file
24
template/conf/wg.conf
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
[Interface]
|
||||||
|
Address = {{ .Server.Address|html }}
|
||||||
|
ListenPort = {{ .Server.ListenPort|html }}
|
||||||
|
PrivateKey = {{ .Server.PrivateKey|html }}
|
||||||
|
MTU = {{ .Server.MTU|html }}
|
||||||
|
PostUp = {{ .Server.PostUp|html }}
|
||||||
|
PreDown = {{ .Server.PreDown|html }}
|
||||||
|
PostDown = {{ .Server.PostDown|html }}
|
||||||
|
Table = {{ .Server.Table|html }}
|
||||||
|
|
||||||
|
{{ range .Clients }}{{ if eq .Enabled true }}
|
||||||
|
# ID: {{ .ID|html }}
|
||||||
|
# Name: {{ .Name|html }}
|
||||||
|
# Emil: {{ .Email|html }}
|
||||||
|
# CreatedAt: {{ .CreatedAt|html }}
|
||||||
|
# UpdatedAt: {{ .UpdatedAt|html }}
|
||||||
|
# CreateUser: {{ .CreateUser|html }}
|
||||||
|
[Peer]
|
||||||
|
PublicKey = {{ .PublicKey|html }}
|
||||||
|
PresharedKey = {{ .PresharedKey|html }}
|
||||||
|
AllowedIPs = {{ .AllowedIPS|html }}
|
||||||
|
PersistentKeepalive = {{ .PersistentKeepalive|html }}
|
||||||
|
{{ if .Endpoint }}Endpoint = {{ .Endpoint|html }}{{ end }}
|
||||||
|
{{ end }}{{ end }}
|
58
template/render_data/render_data.go
Normal file
58
template/render_data/render_data.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package render_data
|
||||||
|
|
||||||
|
type ServerSetting struct {
|
||||||
|
EndpointAddress string `json:"endpointAddress"`
|
||||||
|
DnsServer []string `json:"dnsServer"`
|
||||||
|
MTU int `json:"MTU"`
|
||||||
|
PersistentKeepalive int `json:"persistentKeepalive"`
|
||||||
|
FirewallMark string `json:"firewallMark"`
|
||||||
|
Table string `json:"table"`
|
||||||
|
ConfigFilePath string `json:"configFilePath"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
Address []string `json:"ipScope"`
|
||||||
|
ListenPort uint64 `json:"listenPort"`
|
||||||
|
PrivateKey string `json:"privateKey"`
|
||||||
|
PublicKey string `json:"publicKey" `
|
||||||
|
MTU int `json:"MTU"`
|
||||||
|
PostUp string `json:"postUpScript"`
|
||||||
|
PreDown string `json:"preDownScript"`
|
||||||
|
PostDown string `json:"postDownScript"`
|
||||||
|
Table string `json:"table"`
|
||||||
|
Clients []Client `json:"clients"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
PresharedKey string `json:"presharedKey"`
|
||||||
|
AllowedIPS string `json:"allowedIps"`
|
||||||
|
PersistentKeepalive string `json:"persistentKeepalive"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
CreateUser string `json:"createUser"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
UpdatedAt string `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Keys struct {
|
||||||
|
PrivateKey string `json:"privateKey"`
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
PresharedKey string `json:"presharedKey"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientConfig struct {
|
||||||
|
PrivateKey string `json:"privateKey"`
|
||||||
|
IpAllocation string `json:"ipAllocation"`
|
||||||
|
MTU int `json:"MTU"`
|
||||||
|
DNS string `json:"DNS"`
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
PresharedKey string `json:"presharedKey"`
|
||||||
|
AllowedIPS string `json:"allowedIPS"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
ListenPort int `json:"listenPort"`
|
||||||
|
PersistentKeepalive int `json:"persistentKeepalive"`
|
||||||
|
}
|
42
utils/avatar.go
Normal file
42
utils/avatar.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
"wireguard-ui/global/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type avatar struct{}
|
||||||
|
|
||||||
|
func Avatar() avatar {
|
||||||
|
return avatar{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateAvatar
|
||||||
|
// @description: 生成随机头像 - 默认头像
|
||||||
|
// @receiver avatar
|
||||||
|
// @return path
|
||||||
|
// @return err
|
||||||
|
func (avatar) GenerateAvatar(isUpload bool) (path string, err error) {
|
||||||
|
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
r := client.HttpClient.R()
|
||||||
|
result, err := r.Get(fmt.Sprintf("https://api.dicebear.com/7.x/croodles/png?seed=%d&scale=120&size=200&clip=true&randomizeIds=true&beard=variant01,variant02,variant03&"+
|
||||||
|
"eyes=variant01,variant02,variant03,variant04,variant05,variant06,variant07,variant08,variant09,variant10,variant11,variant12&mustache=variant01,variant02,variant03&"+
|
||||||
|
"topColor=000000,0fa958,699bf7", rand.Uint32()))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("请求头像API失败: %v", err.Error())
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isUpload {
|
||||||
|
path, err = FileSystem().UploadFile(result.Body(), ".png")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(result.Body()), nil
|
||||||
|
}
|
47
utils/file_system.go
Normal file
47
utils/file_system.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
"wireguard-ui/config"
|
||||||
|
"wireguard-ui/global/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fileSystem struct{}
|
||||||
|
|
||||||
|
func FileSystem() fileSystem {
|
||||||
|
return fileSystem{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadFile
|
||||||
|
// @description: 上传文件
|
||||||
|
// @receiver fileSystem
|
||||||
|
// @param file
|
||||||
|
// @return filePath
|
||||||
|
// @return err
|
||||||
|
func (fileSystem) UploadFile(file []byte, suffix string) (filePath string, err error) {
|
||||||
|
switch config.Config.File.Type {
|
||||||
|
case "oss":
|
||||||
|
ossObj, err := client.OSS.Put(config.Config.File.Path, file, suffix)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ossObj.LongPath, nil
|
||||||
|
case "local":
|
||||||
|
filePath = fmt.Sprintf("%v/%d-avatar%s", config.Config.File.Path, time.Now().Unix(), suffix)
|
||||||
|
// 创建目录
|
||||||
|
if err = os.MkdirAll(filePath, os.FileMode(0777)); err != nil {
|
||||||
|
log.Errorf("本地存储目录创建失败: %v", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err = os.WriteFile(filePath, file, os.FileMode(0777)); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filePath, nil
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
82
utils/mail.go
Normal file
82
utils/mail.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/jordan-wright/email"
|
||||||
|
"mime"
|
||||||
|
"net/smtp"
|
||||||
|
"net/textproto"
|
||||||
|
"path/filepath"
|
||||||
|
"wireguard-ui/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mail struct {
|
||||||
|
*email.Email
|
||||||
|
addr string
|
||||||
|
auth smtp.Auth
|
||||||
|
}
|
||||||
|
|
||||||
|
func Mail() *mail {
|
||||||
|
var m mail
|
||||||
|
em := email.NewEmail()
|
||||||
|
m.Email = em
|
||||||
|
m.auth = smtp.PlainAuth("", config.Config.Mail.User, config.Config.Mail.Password, config.Config.Mail.Host)
|
||||||
|
m.addr = fmt.Sprintf("%s:%d", config.Config.Mail.Host, config.Config.Mail.Port)
|
||||||
|
return &m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mail) VerifyConfig() (err error) {
|
||||||
|
if m == nil {
|
||||||
|
return errors.New("邮件客户端初始化失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.auth == nil || m.addr == "" {
|
||||||
|
return errors.New("邮件客户端未完成初始化")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMail
|
||||||
|
// @description: 发送附件
|
||||||
|
// @receiver m
|
||||||
|
// @param to
|
||||||
|
// @param subject
|
||||||
|
// @param attacheFilePath
|
||||||
|
// @return err
|
||||||
|
func (m *mail) SendMail(to, subject, content, attacheFilePath string) (err error) {
|
||||||
|
m.From = fmt.Sprintf("wg-dashboard <%s>", config.Config.Mail.User)
|
||||||
|
m.To = []string{to}
|
||||||
|
m.Subject = subject
|
||||||
|
m.Text = []byte(content)
|
||||||
|
if attacheFilePath != "" {
|
||||||
|
atch, err := m.AttachFile(attacheFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("读取附件文件失败: %v", err.Error())
|
||||||
|
}
|
||||||
|
emHeader := textproto.MIMEHeader{}
|
||||||
|
emHeader.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, mime.BEncoding.Encode("UTF-8", m.getFileName(attacheFilePath))))
|
||||||
|
atch.Header = emHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Config.Mail.SkipTls {
|
||||||
|
return m.Send(m.addr, m.auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{}
|
||||||
|
tlsConfig.InsecureSkipVerify = config.Config.Mail.SkipTls
|
||||||
|
tlsConfig.ServerName = config.Config.Mail.Host
|
||||||
|
|
||||||
|
return m.SendWithTLS(m.addr, m.auth, tlsConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFileName
|
||||||
|
// @description: 获取文件名
|
||||||
|
// @receiver m
|
||||||
|
// @param filePath
|
||||||
|
// @return string
|
||||||
|
func (m *mail) getFileName(filePath string) string {
|
||||||
|
return filepath.Base(filePath)
|
||||||
|
}
|
128
utils/network.go
Normal file
128
utils/network.go
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"wireguard-ui/global/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type network struct{}
|
||||||
|
|
||||||
|
func Network() network {
|
||||||
|
return network{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHostPublicIP
|
||||||
|
// @description: 获取本机公网地址
|
||||||
|
// @receiver network
|
||||||
|
// @return string
|
||||||
|
func (network) GetHostPublicIP() string {
|
||||||
|
var response = map[string]string{}
|
||||||
|
_, err := client.HttpClient.R().SetResult(&response).Get("https://httpbin.org/ip")
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("获取本机公网IP失败: %v", err.Error())
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return response["origin"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPContains
|
||||||
|
// @description: 校验ip是否在指定IP段中
|
||||||
|
// @receiver network
|
||||||
|
// @param specIp
|
||||||
|
// @param checkIP
|
||||||
|
func (network) IPContains(specIp []string, checkIP string) bool {
|
||||||
|
var isContains bool
|
||||||
|
for _, sip := range specIp {
|
||||||
|
ip, _, _ := net.ParseCIDR(checkIP)
|
||||||
|
sipNet, _, _ := net.ParseCIDR(sip)
|
||||||
|
ipNet := net.IPNet{IP: sipNet, Mask: net.CIDRMask(24, 32)}
|
||||||
|
|
||||||
|
if ipNet.Contains(ip) {
|
||||||
|
isContains = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isContains
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPContainsByIP
|
||||||
|
// @description: 校验ip是否在指定IP段中
|
||||||
|
// @receiver network
|
||||||
|
// @param specIp
|
||||||
|
// @param checkIP
|
||||||
|
func (network) IPContainsByIP(specIp string, checkIP string) bool {
|
||||||
|
ip, _, _ := net.ParseCIDR(checkIP)
|
||||||
|
sipNet, _, _ := net.ParseCIDR(specIp)
|
||||||
|
ipNet := net.IPNet{IP: sipNet, Mask: net.CIDRMask(24, 32)}
|
||||||
|
|
||||||
|
if ipNet.Contains(ip) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseIP
|
||||||
|
// @description: 将带有mark的ip解析
|
||||||
|
// @receiver network
|
||||||
|
// @param ip
|
||||||
|
// @return string
|
||||||
|
func (network) ParseIP(ip string) string {
|
||||||
|
ipn, _, _ := net.ParseCIDR(ip)
|
||||||
|
return ipn.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIPSuffix
|
||||||
|
// @description: 获取IP后缀 [10.10.10.23] 这里只获取23
|
||||||
|
// @receiver network
|
||||||
|
// @param ip
|
||||||
|
// @return string
|
||||||
|
func (network) GetIPSuffix(ip string) string {
|
||||||
|
if strings.Contains(ip, "/") {
|
||||||
|
ip = strings.Split(ip, "/")[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再次拆分,只取最后一段
|
||||||
|
return strings.Split(ip, ".")[3]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIPPrefix
|
||||||
|
// @description: 获取IP前缀[10.10.10.23] 这里只获取 10.10.10
|
||||||
|
// @receiver network
|
||||||
|
// @param ip
|
||||||
|
// @return string
|
||||||
|
func (network) GetIPPrefix(ip string) string {
|
||||||
|
if strings.Contains(ip, "/") {
|
||||||
|
ip = strings.Split(ip, "/")[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(strings.Split(ip, ".")[:3], ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateIPByIPS
|
||||||
|
// @description: 根据指定IP段分配IP
|
||||||
|
// @receiver n
|
||||||
|
// @param ips
|
||||||
|
// @param assignedIPS
|
||||||
|
// @return string
|
||||||
|
func (n network) GenerateIPByIPS(ips []string, assignedIPS ...string) []string {
|
||||||
|
var oips []string
|
||||||
|
for _, sip := range ips {
|
||||||
|
// 再次拆分,只取最后一段
|
||||||
|
suffix := n.GetIPSuffix(sip)
|
||||||
|
prefix := n.GetIPPrefix(sip)
|
||||||
|
for _, cip := range assignedIPS {
|
||||||
|
if n.IPContainsByIP(sip, cip) {
|
||||||
|
suffix = n.GetIPSuffix(cip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
suffix = cast.ToString(cast.ToInt64(suffix) + 1)
|
||||||
|
oips = append(oips, fmt.Sprintf("%s.%s", prefix, suffix))
|
||||||
|
}
|
||||||
|
|
||||||
|
return oips
|
||||||
|
}
|
19
utils/paginate.go
Normal file
19
utils/paginate.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
type paginate struct{}
|
||||||
|
|
||||||
|
func Paginate() paginate {
|
||||||
|
return paginate{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (paginate) Generate(count int64, size int) int {
|
||||||
|
totalPage := 0
|
||||||
|
if count > 0 {
|
||||||
|
upPage := 0
|
||||||
|
if int(count)%size > 0 {
|
||||||
|
upPage = 1
|
||||||
|
}
|
||||||
|
totalPage = (int(count) / size) + upPage
|
||||||
|
}
|
||||||
|
return totalPage
|
||||||
|
}
|
38
utils/password.go
Normal file
38
utils/password.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type password struct{}
|
||||||
|
|
||||||
|
func Password() password {
|
||||||
|
return password{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateHashPassword
|
||||||
|
// @description: 生成hash密码
|
||||||
|
// @receiver password
|
||||||
|
// @param pass
|
||||||
|
// @return string
|
||||||
|
func (password) GenerateHashPassword(pass string) string {
|
||||||
|
bytePass := []byte(pass)
|
||||||
|
hPass, _ := bcrypt.GenerateFromPassword(bytePass, bcrypt.DefaultCost)
|
||||||
|
return string(hPass)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComparePassword
|
||||||
|
// @description: 密码比对
|
||||||
|
// @receiver password
|
||||||
|
// @param dbPass
|
||||||
|
// @param pass
|
||||||
|
// @return bool
|
||||||
|
func (password) ComparePassword(dbPass, pass string) bool {
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(dbPass), []byte(pass)); err != nil {
|
||||||
|
log.Errorf("密码错误: %v", err.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
38
utils/qr_code.go
Normal file
38
utils/qr_code.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"github.com/skip2/go-qrcode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type qrCode struct{}
|
||||||
|
|
||||||
|
func QRCode() qrCode {
|
||||||
|
return qrCode{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateQrCodeBase64
|
||||||
|
// @description: 生成二维码
|
||||||
|
// @receiver qr
|
||||||
|
// @param content
|
||||||
|
// @param size
|
||||||
|
// @return imgStr
|
||||||
|
// @return err
|
||||||
|
func (qr qrCode) GenerateQrCodeBase64(content []byte, size int) (imgStr string, err error) {
|
||||||
|
q, err := qrcode.New(string(content), qrcode.Highest)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("初始化二维码对象失败: %v", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q.DisableBorder = false
|
||||||
|
|
||||||
|
png, err := q.PNG(size)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("生成二维码失败: %v", err.Error())
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
imgStr = "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
|
||||||
|
return
|
||||||
|
}
|
26
utils/rand.go
Normal file
26
utils/rand.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
type random struct{}
|
||||||
|
|
||||||
|
func Random() random {
|
||||||
|
return random{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (random) RandStr(len int) string {
|
||||||
|
var container string
|
||||||
|
var str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||||
|
b := bytes.NewBufferString(str)
|
||||||
|
length := b.Len()
|
||||||
|
bigInt := big.NewInt(int64(length))
|
||||||
|
for i := 0; i < len; i++ {
|
||||||
|
randomInt, _ := rand.Int(rand.Reader, bigInt)
|
||||||
|
container += string(str[randomInt.Int64()])
|
||||||
|
}
|
||||||
|
return container
|
||||||
|
}
|
17
utils/website.go
Normal file
17
utils/website.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "net/url"
|
||||||
|
|
||||||
|
type website struct{}
|
||||||
|
|
||||||
|
func WebSite() website {
|
||||||
|
return website{}
|
||||||
|
}
|
||||||
|
func (website) GetHost(addr string) string {
|
||||||
|
uu, err := url.Parse(addr)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return uu.Host
|
||||||
|
}
|
1
web
Submodule
1
web
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit c6b0aa2467633861b82e1483a6a3f2daf868d686
|
Loading…
x
Reference in New Issue
Block a user