From b245c26515330a25ea5743da4cae582fea7849b1 Mon Sep 17 00:00:00 2001 From: coward Date: Tue, 24 Dec 2024 16:35:57 +0800 Subject: [PATCH 1/2] =?UTF-8?q?:new:=E6=96=B0=E5=A2=9Etui=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=A8=A1=E5=BC=8F[=E8=83=BD=E7=94=A8=E5=B0=B1?= =?UTF-8?q?=E8=A1=8C]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cli/kernal.go | 5 + cli/tui/app.go | 165 ++++++++++ cli/tui/client.go | 520 +++++++++++++++++++++++++++++++ cli/tui/server.go | 62 ++++ cli/tui/setting.go | 440 ++++++++++++++++++++++++++ cli/tui/utils.go | 136 ++++++++ component/jwt.go | 39 ++- global/constant/redis_key.go | 1 + go.mod | 15 + go.sum | 36 +++ http/api/login.go | 2 +- http/api/setting.go | 2 +- http/middleware/authorization.go | 4 +- http/vo/setting.go | 13 +- initialize/init.go | 10 +- service/setting.go | 2 +- utils/qr_code.go | 43 +++ 17 files changed, 1475 insertions(+), 20 deletions(-) create mode 100644 cli/tui/app.go create mode 100644 cli/tui/client.go create mode 100644 cli/tui/server.go create mode 100644 cli/tui/setting.go create mode 100644 cli/tui/utils.go diff --git a/cli/kernal.go b/cli/kernal.go index a0969b9..22e3427 100644 --- a/cli/kernal.go +++ b/cli/kernal.go @@ -1,5 +1,10 @@ package cli +import ( + "wireguard-ui/cli/tui" +) + func Kernel() error { + tui.NewApp().Run() return nil } diff --git a/cli/tui/app.go b/cli/tui/app.go new file mode 100644 index 0000000..171f7eb --- /dev/null +++ b/cli/tui/app.go @@ -0,0 +1,165 @@ +package tui + +import ( + "context" + "errors" + "fmt" + "github.com/google/uuid" + "time" + "wireguard-ui/component" + "wireguard-ui/global/client" + "wireguard-ui/global/constant" + "wireguard-ui/http/vo" + "wireguard-ui/service" + "wireguard-ui/utils" +) + +type App struct { + TokenSecret string // token密钥 + User *vo.User // 登陆用户 + Client *ClientComponent // 客户端组件 + Server *ServerComponent // 服务端组件 + Setting *SettingComponent // 设置组件 +} + +func NewApp() *App { + app := &App{} + if _, err := app.Login(); err != nil { + fmt.Println("登陆失败: ", err) + return nil + } + err := app.AuthLogin() + if err != nil { + fmt.Println("登陆失败: ", err) + return nil + } + fmt.Println("\n=============== 登陆成功 ==============================================================================") + app.Client = NewClientComponent(app.User) + app.Server = NewServerComponent(app.User) + app.Setting = NewSettingComponent(app.User) + fmt.Println("=============== 欢迎使用wireguard-tui =================================================================") + fmt.Println("=============== 当前用户: ", app.User.Nickname, " ================================================================") + fmt.Println("=============== 当前时间: ", time.Now().Format("2006-01-02 15:04:05"), " =======================================================") + fmt.Println("=============== 注意事项如下: =========================================================================") + fmt.Println("=============== 1. 请确保服务端已经安装wireguard ======================================================") + fmt.Println("=============== 2. 请确保服务端和客户端配置文件路径正确 ===============================================") + fmt.Println("=============== 3. 请确保服务端和客户端配置文件权限正确 ===============================================") + fmt.Println("=============== 4. 请确保服务端和客户端配置文件内容正确 ===============================================") + fmt.Println("=============== 5. 请勿泄露配置文件内容 ===============================================================") + fmt.Println("=============== 6. 每次修改客户端、服务端配置或者全局配置过后,请使用重启功能重启服务端,以保证生效 ===") + fmt.Println("=============== 7. 当使用重启无效时,请手动执行对应命令 ===============================================") + fmt.Println("=============== 8. 请勿随意删除客户端,删除后无法恢复 =================================================") + fmt.Println("=============== 9. 手动命令 ===========================================================================") + fmt.Println("=============== 10. 启动wireguard服务端: wg-quick up wg0 ==============================================") + fmt.Println("=============== 11. 停止wireguard服务端: wg-quick down wg0 ============================================") + + return app +} + +func (a *App) Run() { + if a == nil { + return + } + for { + + PrintMenu() + chooseMenu := readInput("请选择菜单: ") + switch chooseMenu { + case "1": + a.Client.ConnectList() + case "2": + a.Client.Menus() + case "3": + a.Server.Menus() + case "4": + a.Setting.Menus() + case "q": + return + } + } +} + +// AuthLogin +// @description: 登陆认证 +// @receiver a +// @return string +func (a *App) AuthLogin() error { + // 先判断token是否存在 + tokenStr, err := client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.TUIUserToken, a.User.Id)).Result() + if err != nil { + // 不存在,去登陆 + tokenStr, err = a.Login() + if err != nil { + return err + } + } + + // 存在,不必要再次登陆,解析token + claims, err := component.JWT().ParseToken("Bearer "+tokenStr, a.TokenSecret, "tui") + if err != nil { + return err + } + + user, err := service.User().GetUserById(claims.ID) + if err != nil { + return err + } + + if user.Status != constant.Enabled { + return errors.New("用户状态异常,请联系管理员处理") + } + + a.User = &vo.User{ + Id: user.Id, + Account: user.Account, + Nickname: user.Nickname, + Avatar: user.Avatar, + Contact: user.Contact, + IsAdmin: user.IsAdmin, + Status: user.Status, + } + + return nil +} + +// Login +// @description: 登陆 +// @receiver a +// @return string +func (a *App) Login() (tokenStr string, err error) { + fmt.Println("============== 登陆 ==============") + + username := readInput("请输入用户名: ") + password := readInput("请输入密码: ") + // 验证码正确,查询用户信息 + user, err := service.User().GetUserByAccount(username) + if err != nil { + return "", fmt.Errorf("用户不存在: %v", err) + } + + // 对比密码 + if !utils.Password().ComparePassword(user.Password, password) { + return "", errors.New("密码错误") + } + + secret := component.JWT().GenerateSecret(password, uuid.NewString(), time.Now().Local().String()) + // 生成token + token, _, err := component.JWT().GenerateToken(user.Id, secret, "tui") + if err != nil { + return "", fmt.Errorf("登陆失败: %v", err) + } + + a.User = &vo.User{ + Id: user.Id, + Account: user.Account, + Nickname: user.Nickname, + Avatar: user.Avatar, + Contact: user.Contact, + IsAdmin: user.IsAdmin, + Status: user.Status, + } + + a.TokenSecret = secret + + return "Bearer " + token, nil +} diff --git a/cli/tui/client.go b/cli/tui/client.go new file mode 100644 index 0000000..aa8dd8e --- /dev/null +++ b/cli/tui/client.go @@ -0,0 +1,520 @@ +package tui + +import ( + "encoding/json" + "fmt" + "gitee.ltd/lxh/logger/log" + "github.com/charmbracelet/bubbles/table" + "github.com/spf13/cast" + "os" + "strconv" + "strings" + "time" + "wireguard-ui/component" + "wireguard-ui/global/constant" + "wireguard-ui/http/param" + "wireguard-ui/http/vo" + "wireguard-ui/service" + "wireguard-ui/utils" +) + +type ClientComponent struct { + LoginUser *vo.User + Menu []string + Clients [][]string +} + +func NewClientComponent(loginUser *vo.User) *ClientComponent { + ccp := &ClientComponent{ + LoginUser: loginUser, + Menu: []string{"[1] 客户端列表", "[2] 查看客户端配置", "[3] 添加客户端", "[4] 编辑客户端", "[5] 删除客户端", "[q] 返回上一级菜单"}, + } + + return ccp +} + +// Menus +// @description: 客户端菜单 +// @receiver c +func (c *ClientComponent) Menus() { + fmt.Println("") + for _, r := range c.Menu { + fmt.Println(" -> " + r) + } + + chooseMenu := readInput("\n请选择: ") + switch chooseMenu { + case "1": + c.List(true) + case "2": + c.ShowConfig() + case "3": + c.Add() + case "4": + c.Edit() + case "5": + c.Delete() + case "q": + return + } +} + +// ConnectList +// @description: 客户端链接列表 +// @receiver c +// @return string +func (c *ClientComponent) ConnectList() { + fmt.Println("\n客户端链接列表") + connectList, err := component.Wireguard().GetClients() + if err != nil { + fmt.Println("获取客户端链接列表失败: ", err.Error()) + return + } + + var data [][]string + for _, peer := range connectList { + // 获取客户端链接信息 + clientInfo, err := service.Client().GetByPublicKey(peer.PublicKey.String()) + if err != nil { + continue + } + var ipAllocation string + for _, iaip := range peer.AllowedIPs { + ipAllocation += iaip.String() + "," + } + // 去除一下最右边的逗号 + if len(ipAllocation) > 0 { + ipAllocation = strings.TrimRight(ipAllocation, ",") + } + var isOnline = "否" + if time.Since(peer.LastHandshakeTime).Minutes() < 3 { + isOnline = "是" + } + data = append(data, []string{clientInfo.Name, + clientInfo.Email, + ipAllocation, + isOnline, + utils.FlowCalculation().Parse(peer.TransmitBytes), + utils.FlowCalculation().Parse(peer.ReceiveBytes), + peer.Endpoint.String(), + peer.LastHandshakeTime.Format("2006-01-02 15:04:05")}) + } + + //if len(data) <= 0 { + // //data = append(data, []string{"暂无数据"}) + // // data = append(data, []string{"名称1", "12345678910@qq.com", "192.168.100.1", "是", "10G", "20G", "1.14.30.133:51280", "2024-12-20 15:07:36"}, []string{"名称2", "12345678910@qq.com", "192.168.100.2", "否", "20G", "40G", "1.14.30.133:51280", "2024-12-22 15:07:36"}) + //} + + title := []table.Column{ + { + Title: "客户端名称", + Width: 20, + }, { + Title: "联系邮箱", + Width: 20, + }, { + Title: "分配的IP", + Width: 30, + }, { + Title: "是否在线", + Width: 10, + }, { + Title: "接收流量", + Width: 10, + }, { + Title: "传输流量", + Width: 10, + }, { + Title: "链接端点", + Width: 30, + }, { + Title: "最后握手时间", + Width: 30, + }} + + Show(GenerateTable(title, data)) + return +} + +// List +// @description: 客户端列表 +// @receiver c +func (c *ClientComponent) List(showMenu bool) { + + title := []table.Column{ + { + Title: "序号", + Width: 5, + }, + { + Title: "ID", + Width: 35, + }, + { + Title: "名称", + Width: 25, + }, + { + Title: "联系邮箱", + Width: 30, + }, + { + Title: "客户端IP", + Width: 20, + }, + { + Title: "状态", + Width: 10, + }, + { + Title: "离线通知", + Width: 10, + }, + } + + records, _, err := service.Client().List(param.ClientList{ + Page: param.Page{ + Current: -1, + }, + }) + if err != nil { + fmt.Println("获取客户端列表失败: ", err.Error()) + return + } + + fmt.Println("\n客户端列表") + var data [][]string + for i, client := range records { + var status, offlineNotify string + if client.Enabled == 1 { + status = "启用" + } else { + status = "禁用" + } + + if client.OfflineMonitoring == 1 { + offlineNotify = "开启" + } else { + offlineNotify = "关闭" + } + + data = append(data, []string{ + cast.ToString(i + 1), + client.Id, + client.Name, + client.Email, + client.IpAllocationStr, + status, + offlineNotify, + }) + } + + Show(GenerateTable(title, data)) + c.Clients = data + if showMenu { + c.Menus() + } + return +} + +// ShowConfig +// @description: 显示配置 +// @receiver c +func (c *ClientComponent) ShowConfig() { + c.List(false) + + clientIdx := readInput("请输入客户端序号: ") + downloadType := readInput("请输入下载类型(FILE - 文件 | EMAIL - 邮件): ") + idx, err := strconv.Atoi(clientIdx) + if err != nil { + fmt.Println("输入有误: ", err.Error()) + return + } + + if idx < 1 || idx > len(c.Clients) { + fmt.Println("输入有误") + return + } + + client := c.Clients[idx-1] + // 取到id + clientID := client[1] + // 查询客户端信息 + clientInfo, err := service.Client().GetByID(clientID) + if err != nil { + fmt.Println("获取客户端信息失败: ", err.Error()) + return + } + + // 渲染配置 + var keys vo.Keys + _ = json.Unmarshal([]byte(clientInfo.Keys), &keys) + + globalSet, err := service.Setting().GetWGSetForConfig() + if err != nil { + fmt.Println("获取全局配置失败: ", err.Error()) + return + } + + serverConf, err := service.Setting().GetWGServerForConfig() + if err != nil { + fmt.Println("获取服务器配置失败: ", err.Error()) + return + } + + outPath, err := component.Wireguard().GenerateClientFile(clientInfo, serverConf, globalSet) + if err != nil { + fmt.Println("生成客户端配置失败: ", err.Error()) + return + } + + // 根据不同下载类型执行不同逻辑 + switch downloadType { + case "FILE": // 二维码 + // 读取文件内容 + fileContent, err := os.ReadFile(outPath) + if err != nil { + fmt.Println("读取文件失败: ", err.Error()) + return + } + + fmt.Println("\n#请将以下内容复制到客户端配置文件中【不包含本行提示语】") + fmt.Println("\n" + string(fileContent)) + + if err = os.Remove(outPath); err != nil { + log.Errorf("删除临时文件失败: %s", err.Error()) + } + + case "EMAIL": // 邮件 + if clientInfo.Email == "" { + fmt.Println("当前客户端并未配置通知邮箱") + return + } + + // 获取邮箱配置 + emailConf, err := service.Setting().GetByCode("EMAIL_SMTP") + if err != nil { + fmt.Println("获取邮箱配置失败,请先到设置页面的【其他】里面添加code为【EMAIL_SMTP】的具体配置") + return + } + + err = utils.Mail(emailConf).SendMail(clientInfo.Email, fmt.Sprintf("客户端: %s", clientInfo.Name), "请查收附件", outPath) + if err != nil { + fmt.Println("发送邮件失败") + return + } + + if err = os.Remove(outPath); err != nil { + log.Errorf("删除临时文件失败: %s", err.Error()) + } + + fmt.Println("发送邮件成功,请注意查收!") + } + + c.Menus() +} + +// Add +// @description: 添加客户端 +// @receiver c +func (c *ClientComponent) Add() { + fmt.Println("\n添加客户端") + clientIP, serverIP, err := GenerateIP() + if err != nil { + fmt.Println("生成客户端IP失败: ", err.Error()) + return + } + + keys, err := GenerateKeys() + if err != nil { + fmt.Println("生成密钥对失败: ", err.Error()) + return + } + + var p param.SaveClient + p.Name = readInput("请输入客户端名称: ") + p.Email = readInput("请输入联系邮箱: ") + clientIPIn := readInput("请输入客户端IP(默认自动生成,多个采用 ',' 分割,例如 10.10.0.1/32,10.10.0.2/32): ") + if clientIPIn == "" { + p.IpAllocation = clientIP + } else { + p.IpAllocation = strings.Split(clientIPIn, ",") + } + + p.AllowedIps = serverIP + p.Keys = ¶m.Keys{ + PrivateKey: keys.PrivateKey, + PublicKey: keys.PublicKey, + PresharedKey: keys.PresharedKey, + } + + var useServerDNS, enabled, offlineNotify constant.Status + + useServerDNSIn := readInput("是否使用服务器DNS(默认不使用 1 - 是 | 0 - 否 ): ") + switch useServerDNSIn { + case "1": + useServerDNS = constant.Enabled + case "0": + useServerDNS = constant.Disabled + default: + useServerDNS = constant.Disabled + } + + enabledIn := readInput("是否启用(默认启用 1 - 是 | 0 - 否 ): ") + switch enabledIn { + case "1": + enabled = constant.Enabled + case "0": + enabled = constant.Disabled + default: + enabled = constant.Enabled + } + + offlineNotifyIn := readInput("是否开启离线通知(默认关闭 1 - 是 | 0 - 否 ): ") + switch offlineNotifyIn { + case "1": + offlineNotify = constant.Enabled + case "0": + offlineNotify = constant.Disabled + default: + offlineNotify = constant.Disabled + } + p.UseServerDns = &useServerDNS + p.Enabled = &enabled + p.OfflineMonitoring = &offlineNotify + + err = service.Client().SaveClient(p, c.LoginUser) + if err != nil { + fmt.Println("添加客户端失败: ", err.Error()) + return + } + + fmt.Println("添加客户端成功") + c.List(true) + return +} + +// Edit +// @description: 编辑客户端 +// @receiver c +func (c *ClientComponent) Edit() { + fmt.Println("\n编辑客户端") + c.List(false) + + clientIdx := readInput("请输入客户端序号: ") + idx, err := strconv.Atoi(clientIdx) + if err != nil { + fmt.Println("输入有误: ", err.Error()) + return + } + + if idx < 1 || idx > len(c.Clients) { + fmt.Println("输入有误") + return + } + + client := c.Clients[idx-1] + // 取到id + clientID := client[1] + // 查询客户端信息 + clientInfo, err := service.Client().GetByID(clientID) + if err != nil { + fmt.Println("获取客户端信息失败: ", err.Error()) + return + } + + var p param.SaveClient + p.Id = clientID + p.Name = readInput("请输入客户端名称[无需改变请回车跳过,下同]: ") + p.Email = readInput("请输入联系邮箱: ") + clientIPIn := readInput("请输入客户端IP(默认自动生成,多个采用 ',' 分割,例如 10.10.0.1/32,10.10.0.2/32): ") + if clientIPIn == "" { + p.IpAllocation = strings.Split(clientInfo.IpAllocation, ",") + } else { + p.IpAllocation = strings.Split(clientIPIn, ",") + } + + p.AllowedIps = strings.Split(clientInfo.AllowedIps, ",") + var keys *param.Keys + _ = json.Unmarshal([]byte(clientInfo.Keys), &keys) + + p.Keys = keys + + var useServerDNS, enabled, offlineNotify constant.Status + + useServerDNSIn := readInput("是否使用服务器DNS(默认不使用 1 - 是 | 0 - 否 ): ") + switch useServerDNSIn { + case "1": + useServerDNS = constant.Enabled + case "0": + useServerDNS = constant.Disabled + default: + useServerDNS = clientInfo.UseServerDns + } + + enabledIn := readInput("是否启用(默认启用 1 - 是 | 0 - 否 ): ") + switch enabledIn { + case "1": + enabled = constant.Enabled + case "0": + enabled = constant.Disabled + default: + enabled = clientInfo.Enabled + } + + offlineNotifyIn := readInput("是否开启离线通知(默认关闭 1 - 是 | 0 - 否 ): ") + switch offlineNotifyIn { + case "1": + offlineNotify = constant.Enabled + case "0": + offlineNotify = constant.Disabled + default: + offlineNotify = clientInfo.OfflineMonitoring + } + p.UseServerDns = &useServerDNS + p.Enabled = &enabled + p.OfflineMonitoring = &offlineNotify + + err = service.Client().SaveClient(p, c.LoginUser) + if err != nil { + fmt.Println("编辑客户端失败: ", err.Error()) + return + } + + fmt.Println("编辑客户端成功") + c.List(true) + return +} + +// Delete +// @description: 删除客户端 +// @receiver c +func (c *ClientComponent) Delete() { + fmt.Println("\n删除客户端") + + c.List(false) + + clientIdx := readInput("请输入客户端序号: ") + idx, err := strconv.Atoi(clientIdx) + if err != nil { + fmt.Println("输入有误: ", err.Error()) + return + } + + if idx < 1 || idx > len(c.Clients) { + fmt.Println("输入有误") + return + } + + client := c.Clients[idx-1] + // 取到id + clientID := client[1] + if err := service.Client().Delete(clientID); err != nil { + fmt.Println("删除客户端失败: ", err.Error()) + return + } + + c.List(true) + fmt.Println("删除客户端成功") + return +} diff --git a/cli/tui/server.go b/cli/tui/server.go new file mode 100644 index 0000000..d249e07 --- /dev/null +++ b/cli/tui/server.go @@ -0,0 +1,62 @@ +package tui + +import ( + "fmt" + "wireguard-ui/command" + "wireguard-ui/http/vo" +) + +type ServerComponent struct { + LoginUser *vo.User + Menu []string +} + +func NewServerComponent(loginUser *vo.User) *ServerComponent { + return &ServerComponent{ + LoginUser: loginUser, + Menu: []string{"[1] 启动服务", "[2] 关闭服务", "[3] 重启服务", "[q] 返回上一级菜单"}, + } +} + +// Menus +// @description: 服务端菜单 +// @receiver s +func (s *ServerComponent) Menus() { + fmt.Println("") + for _, r := range s.Menu { + fmt.Println(" -> " + r) + } + + chooseMenu := readInput("\n请选择: ") + switch chooseMenu { + case "1": + s.Start() + case "2": + s.Stop() + case "3": + s.Restart() + case "q": + return + } +} + +// Start +// @description: 启动服务 +// @receiver s +func (s *ServerComponent) Start() { + command.StartWireguard("") +} + +// Stop +// @description: 停止服务 +// @receiver s +func (s *ServerComponent) Stop() { + command.StopWireguard("") +} + +// Restart +// @description: 重启服务 +// @receiver s +func (s *ServerComponent) Restart() { + command.RestartWireguard(false, "") +} diff --git a/cli/tui/setting.go b/cli/tui/setting.go new file mode 100644 index 0000000..e861a8d --- /dev/null +++ b/cli/tui/setting.go @@ -0,0 +1,440 @@ +package tui + +import ( + "encoding/json" + "fmt" + "github.com/charmbracelet/bubbles/table" + "github.com/eiannone/keyboard" + "github.com/spf13/cast" + "strings" + "wireguard-ui/http/vo" + "wireguard-ui/model" + "wireguard-ui/service" + "wireguard-ui/utils" +) + +type SettingComponent struct { + LoginUser *vo.User + Menu []string + Other *OtherSettingComponent +} + +func NewSettingComponent(loginUser *vo.User) *SettingComponent { + return &SettingComponent{ + LoginUser: loginUser, + Menu: []string{"[1] 服务端配置", "[2] 全局设置", "[3] 其他配置", "[q] 返回上一级菜单"}, + Other: NewOtherSettingComponent(), + } +} + +// Menus +// @description: 设置菜单 +// @receiver s +func (s *SettingComponent) Menus() { + fmt.Println("") + for _, r := range s.Menu { + fmt.Println(" -> " + r) + } + + chooseMenu := readInput("\n请选择: ") + switch chooseMenu { + case "1": + s.ServerSetting() + case "2": + s.GlobalSetting() + case "3": + s.Other.Menus(s) + case "q": + return + } + +} + +// ServerSetting +// @description: 服务端配置 +// @receiver s +func (s *SettingComponent) ServerSetting() { + fmt.Println("\n服务端配置") + // 先读取一下服务端配置 + servConf, err := service.Setting().GetByCode("WG_SERVER") + if err != nil { + fmt.Println("获取服务端配置失败: ", err) + return + } + + type serverConf struct { + IpScope []string `json:"ipScope"` + ListenPort int `json:"listenPort"` + PrivateKey string `json:"privateKey"` + PublicKey string `json:"publicKey"` + PostUpScript string `json:"postUpScript"` + PostDownScript string `json:"postDownScript"` + } + + // 解析出来好渲染 + var conf serverConf + if err = json.Unmarshal([]byte(servConf.Data), &conf); err != nil { + fmt.Println("解析服务端配置失败: ", err) + return + } + + ipScopeIn := readInput(fmt.Sprintf("请输入IP段多个采用 ',[英文逗号]' 分割,不填写默认当前值,下同,当前值[%s] :", strings.Replace(strings.Join(conf.IpScope, ","), " ", "", -1))) + listenPortIn := readInput(fmt.Sprintf("请输入监听端口,当前值[%d]: ", conf.ListenPort)) + privateKeyIn := readInput(fmt.Sprintf("请输入私钥,当前值[%s]: ", conf.PrivateKey)) + publicKeyIn := readInput(fmt.Sprintf("请输入公钥,当前值[%s]: ", conf.PublicKey)) + postUpScriptIn := readInput(fmt.Sprintf("请输入PostUp脚本,当前值[%s]: ", conf.PostUpScript)) + postDownScriptIn := readInput(fmt.Sprintf("请输入PostDown脚本,当前值[%s]: ", conf.PostDownScript)) + + if ipScopeIn != "" { + conf.IpScope = strings.Split(ipScopeIn, ",") + } + if listenPortIn != "" { + conf.ListenPort = cast.ToInt(listenPortIn) + } + if privateKeyIn != "" { + conf.PrivateKey = privateKeyIn + } + if publicKeyIn != "" { + conf.PublicKey = publicKeyIn + } + if postUpScriptIn != "" { + conf.PostUpScript = postUpScriptIn + } + if postDownScriptIn != "" { + conf.PostDownScript = postDownScriptIn + } + + data, _ := json.Marshal(conf) + + if err := service.Setting().SetData(&model.Setting{ + Code: "WG_SERVER", + Data: string(data), + }); err != nil { + fmt.Println("保存服务端配置失败: ", err) + return + } + + fmt.Println("修改服务端配置成功") + return +} + +// GlobalSetting +// @description: 全局设置 +// @receiver s +func (s *SettingComponent) GlobalSetting() { + fmt.Println("\n服务端配置") + // 先读取一下服务端配置 + globalConf, err := service.Setting().GetByCode("WG_SETTING") + if err != nil { + fmt.Println("获取服务端配置失败: ", err) + return + } + + type gConf struct { + MTU int `json:"MTU"` + ConfigFilePath string `json:"configFilePath"` + DnsServer []string `json:"dnsServer"` + EndpointAddress string `json:"endpointAddress"` + FirewallMark string `json:"firewallMark"` + PersistentKeepalive int `json:"persistentKeepalive"` + Table string `json:"table"` + } + + // 解析出来好渲染 + var conf gConf + if err = json.Unmarshal([]byte(globalConf.Data), &conf); err != nil { + fmt.Println("解析全局配置失败: ", err) + return + } + + mtu := readInput(fmt.Sprintf("请输入mtu,不填写默认当前值,下同,当前值[%d] :", conf.MTU)) + configFilePath := readInput(fmt.Sprintf("请输入配置文件地址,当前值[%s]: ", conf.ConfigFilePath)) + dnsServer := readInput(fmt.Sprintf("请输入dns,多个采用 ',[英文逗号]' 分割,当前值[%s]: ", strings.Replace(strings.Join(conf.DnsServer, ","), " ", "", -1))) + endpointAddress := readInput(fmt.Sprintf("请输入公网IP,默认系统自动获取,当前值[%s]: ", conf.EndpointAddress)) + firewallMark := readInput(fmt.Sprintf("请输入FirewallMark,当前值[%s]: ", conf.FirewallMark)) + persistentKeepalive := readInput(fmt.Sprintf("请输入PersistentKeepalive,当前值[%d]: ", conf.PersistentKeepalive)) + tableRule := readInput(fmt.Sprintf("请输入Table,当前值[%s]: ", conf.Table)) + + if mtu != "" { + conf.MTU = cast.ToInt(mtu) + } + if configFilePath != "" { + conf.ConfigFilePath = configFilePath + } + if dnsServer != "" { + conf.DnsServer = strings.Split(dnsServer, ",") + } + if endpointAddress != "" { + conf.EndpointAddress = endpointAddress + } else { + conf.EndpointAddress = utils.Network().GetHostPublicIP() + } + if firewallMark != "" { + conf.FirewallMark = firewallMark + } + if persistentKeepalive != "" { + conf.PersistentKeepalive = cast.ToInt(persistentKeepalive) + } + if tableRule != "" { + conf.Table = tableRule + } + + data, _ := json.Marshal(conf) + + if err := service.Setting().SetData(&model.Setting{ + Code: "WG_SETTING", + Data: string(data), + }); err != nil { + fmt.Println("保存全局配置失败: ", err) + return + } + + fmt.Println("修改全局配置成功") + return +} + +// OtherSettingComponent +// @description: 其他配置杂项 +type OtherSettingComponent struct { + Setting *SettingComponent + Menu []string +} + +func NewOtherSettingComponent() *OtherSettingComponent { + return &OtherSettingComponent{ + Menu: []string{"[1] 列表", "[2] 添加", "[3] 编辑", "[4] 删除", "[q] 返回上一级菜单"}, + } +} + +func (s *OtherSettingComponent) Menus(setting *SettingComponent) { + if s.Setting == nil { + s.Setting = setting + } + fmt.Println("") + for _, r := range s.Menu { + fmt.Println(" -> " + r) + } + + chooseMenu := readInput("\n请选择: ") + switch chooseMenu { + case "1": + s.List(true) + case "2": + s.Add() + case "3": + s.Edit() + case "4": + s.Delete() + + case "q": + s.Setting.Menus() + } + +} + +// List +// @description: 其他配置 +// @receiver s +// @param showMenu +func (s *OtherSettingComponent) List(showMenu bool) { + fmt.Println("\n其他配置列表") + // 不查询的配置 + var blackList = []string{"WG_SETTING", "WG_SERVER"} + + data, err := service.Setting().GetAllSetting(blackList) + if err != nil { + fmt.Println("获取配置失败") + return + } + + title := []table.Column{ + { + Title: "序号", + Width: 10, + }, + { + Title: "编码", + Width: 40, + }, + { + Title: "描述", + Width: 50, + }, + { + Title: "创建时间", + Width: 30, + }, + { + Title: "更新时间", + Width: 30, + }, + } + + var result [][]string + for i, v := range data { + result = append(result, []string{ + cast.ToString(i + 1), + v.Code, + v.Describe, + v.CreatedAt.Format("2006-01-02 15:04:05"), + v.UpdatedAt.Format("2006-01-02 15:04:05"), + }) + } + + Show(GenerateTable(title, result)) + if showMenu { + s.Menus(nil) + } +} + +// Add +// @description: 新增其他配置 +// @receiver s +func (s *OtherSettingComponent) Add() { + fmt.Println("\n新增其他配置") + + code := readInput("请输入配置编码,此编码是唯一编码不可重复:") + desc := readInput("请输入配置描述:") + + // 监听键盘事件,只监听 + 和 - 和 enter + if err := keyboard.Open(); err != nil { + return + } + + defer func() { + _ = keyboard.Close() + }() + + // + <=> 43 | - <=> 45 | enter <=> 0 + fmt.Println("请按下 + 或者 - 进行配置项的新增和删除") + fmt.Println("每一项配置如此: key=val ") + fmt.Println("确认输入完成后 enter[按一次代表当前配置项输入完成,两次代表新增完成]") + fmt.Println("首先进入时请输入 + 进行第一个配置项填写") + var breakCycle bool + var keyVal []string + for { + char, _, err := keyboard.GetKey() + if err != nil { + break + } + + if breakCycle { + break + } + + switch char { + case 0: + // 收到enter事件触发,执行后面的 + var dm = make(map[string]any) + for _, kv := range keyVal { + kvs := strings.Split(kv, "=") + key := kvs[0] + val := kvs[1] + dm[key] = val + } + + dms, err := json.Marshal(dm) + if err != nil { + breakCycle = true + continue + } + + if err = service.Setting().SetData(&model.Setting{ + Code: code, + Data: string(dms), + Describe: desc, + }); err != nil { + breakCycle = true + fmt.Println("保存配置失败: ", err.Error()) + continue + } + + fmt.Println("保存配置成功") + s.List(true) + + breakCycle = true + case 43: + keyVal = append(keyVal, readInput("请输入配置项:")) + case 45: + keyVal = keyVal[:len(keyVal)-1] + fmt.Println("已删除最后一个配置项,当前配置项为:", keyVal) + } + + } + +} + +// Edit +// @description: 编辑 +// @receiver s +func (s *OtherSettingComponent) Edit() { + fmt.Println("\n编辑其他配置") + + s.List(false) + + code := readInput("请输入需要编辑的配置编码:") + + // 通过编码查询配置 + setting, err := service.Setting().GetByCode(code) + if err != nil { + fmt.Println("查找["+code+"]配置失败:", err.Error()) + return + } + + desc := readInput("请输入需要编辑的配置描述:") + if desc != "" { + setting.Describe = desc + } + + var kvm = make(map[string]any) + if err = json.Unmarshal([]byte(setting.Data), &kvm); err != nil { + fmt.Println("配置解析失败: ", err.Error()) + return + } + + for k, v := range kvm { + valIn := readInput(fmt.Sprintf("请输入配置项值,仅能修改值,当前键值对:%s=%v :", k, v)) + if valIn != "" { + kvm[k] = valIn + } + } + + // 处理以下数据 + kvmStr, err := json.Marshal(kvm) + if err != nil { + fmt.Println("序列化数据失败: ", err.Error()) + return + } + + setting.Data = string(kvmStr) + + if err = service.Setting().SetData(setting); err != nil { + fmt.Println("修改配置失败: ", err.Error()) + return + } + + fmt.Println("修改配置成功") +} + +// Delete +// @description: 删除 +// @receiver s +func (s *OtherSettingComponent) Delete() { + fmt.Println("\n 删除指定配置") + + s.List(false) + + code := readInput("请输入要删除的配置项编码:") + // 查询配置是否存在 + if err := service.Setting().Model(&model.Setting{}). + Where("code NOT IN (?)", []string{"WG_SETTING", "WG_SERVER"}). + Where("code = ?", code).Delete(&model.Setting{}).Error; err != nil { + fmt.Println("删除[" + code + "]配置失败") + return + } + + fmt.Println("删除成功") + + s.List(true) + +} diff --git a/cli/tui/utils.go b/cli/tui/utils.go new file mode 100644 index 0000000..700eb24 --- /dev/null +++ b/cli/tui/utils.go @@ -0,0 +1,136 @@ +package tui + +import ( + "bufio" + "fmt" + "gitee.ltd/lxh/logger/log" + "github.com/charmbracelet/bubbles/table" + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cast" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + "os" + "strings" + "wireguard-ui/http/vo" + "wireguard-ui/model" + "wireguard-ui/service" + "wireguard-ui/utils" +) + +var BaseStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")) + +var menus = []string{"[1] 客户端链接列表", "[2] 客户端", "[3] 服务端", "[4] 设置", "[q] 退出"} + +// PrintMenu +// @description: 打印一下菜单 +func PrintMenu() { + fmt.Println("\n菜单:") + for _, menu := range menus { + fmt.Println(menu) + } +} + +// GenerateTable +// @description: 生成数据表 +// @param column +// @param rows +// @return string +func GenerateTable(column []table.Column, rows [][]string) string { + var data []table.Row + for _, v := range rows { + data = append(data, v) + } + + tl := table.New( + table.WithColumns(column), + table.WithRows(data), + table.WithHeight(len(data)+1), + ) + + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")). + BorderBottom(true). + Bold(false) + s.Selected = lipgloss.NewStyle() + + tl.SetStyles(s) + + return BaseStyle.Render(tl.View()+"\n") + "\n一共有 【" + fmt.Sprintf("%d", len(data)) + "】 条数据" +} + +// GenerateIP +// @description: 生成IP +// @return clientIPS +// @return serverIPS +// @return err +func GenerateIP() (clientIPS, serverIPS []string, err error) { + // 获取一下服务端信息,因为IP分配需要根据服务端的IP制定 + serverInfo, err := service.Setting().GetWGServerForConfig() + if err != nil { + return nil, nil, err + } + + 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...) + + return ips, serverInfo.Address, nil +} + +// GenerateKeys +// @description: 生成密钥对 +// @return keys +// @return err +func GenerateKeys() (keys *vo.Keys, err error) { + // 为空,新增 + privateKey, err := wgtypes.GeneratePrivateKey() + if err != nil { + return nil, err + } + publicKey := privateKey.PublicKey().String() + presharedKey, err := wgtypes.GenerateKey() + if err != nil { + return nil, fmt.Errorf("生成预共享密钥失败: %s", err.Error()) + } + keys = &vo.Keys{ + PrivateKey: privateKey.String(), + PublicKey: publicKey, + PresharedKey: presharedKey.String(), + } + + return keys, nil +} + +// Show +// @description: 展示 +// @param data +func Show(data any) { + fmt.Println(data) +} + +// readInput +// @description: 读取输入 +// @param prompt +// @return string +func readInput(prompt string) string { + fmt.Print(prompt) + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + return scanner.Text() +} diff --git a/component/jwt.go b/component/jwt.go index e28cdac..5f7daa0 100644 --- a/component/jwt.go +++ b/component/jwt.go @@ -36,7 +36,7 @@ func JWT() JwtComponent { // @return token // @return expireTime // @return err -func (JwtComponent) GenerateToken(userId, secret string, times ...time.Time) (token string, expireTime *jwt.NumericDate, err error) { +func (JwtComponent) GenerateToken(userId, secret, source string, times ...time.Time) (token string, expireTime *jwt.NumericDate, err error) { var notBefore, issuedAt *jwt.NumericDate if len(times) != 0 { expireTime = jwt.NewNumericDate(times[0]) @@ -68,10 +68,19 @@ func (JwtComponent) GenerateToken(userId, secret string, times ...time.Time) (to return "", nil, errors.New("生成token失败") } - client.Redis.Set(context.Background(), - fmt.Sprintf("%s:%s", constant.UserToken, userId), - token, - time.Duration(expireTime.Sub(time.Now()).Abs().Seconds())*time.Second) + switch source { + case "http": + client.Redis.Set(context.Background(), + fmt.Sprintf("%s:%s", constant.UserToken, userId), + token, + time.Duration(expireTime.Sub(time.Now()).Abs().Seconds())*time.Second) + case "tui": + client.Redis.Set(context.Background(), + fmt.Sprintf("%s:%s", constant.TUIUserToken, userId), + token, + time.Duration(expireTime.Sub(time.Now()).Abs().Seconds())*time.Second) + } + return } @@ -81,7 +90,7 @@ func (JwtComponent) GenerateToken(userId, secret string, times ...time.Time) (to // @param token // @return *JwtComponent // @return error -func (JwtComponent) ParseToken(token, secret string) (*JwtComponent, error) { +func (JwtComponent) ParseToken(token, secret, source string) (*JwtComponent, error) { tokenStr := strings.Split(token, "Bearer ")[1] t, err := jwt.ParseWithClaims(tokenStr, &JwtComponent{}, func(token *jwt.Token) (any, error) { @@ -89,10 +98,20 @@ func (JwtComponent) ParseToken(token, secret string) (*JwtComponent, error) { }) 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不存在") + var userToken string + switch source { + case "http": + 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不存在") + } + case "tui": + userToken, err = client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.TUIUserToken, claims.ID)).Result() + if err != nil { + log.Errorf("缓存中用户[%s]的token查找失败: %v", claims.ID, err.Error()) + return nil, errors.New("token不存在") + } } if userToken != tokenStr { diff --git a/global/constant/redis_key.go b/global/constant/redis_key.go index 35c6fa9..18d1cd8 100644 --- a/global/constant/redis_key.go +++ b/global/constant/redis_key.go @@ -3,5 +3,6 @@ package constant const ( Captcha = "captcha" UserToken = "token" + TUIUserToken = "tui:token" ClientOffline = "client:offline:" ) diff --git a/go.mod b/go.mod index 8d57ad3..f8a713a 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,11 @@ go 1.21 require ( gitee.ltd/lxh/logger v1.0.18 + github.com/charmbracelet/bubbles v0.20.0 + github.com/charmbracelet/lipgloss v0.13.0 github.com/cowardmrx/go_aliyun_oss v1.0.7 github.com/dustin/go-humanize v1.0.1 + github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 github.com/fsnotify/fsnotify v1.7.0 github.com/gin-gonic/gin v1.10.0 github.com/glebarez/sqlite v1.11.0 @@ -35,15 +38,20 @@ require ( require ( github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/sonic v1.12.5 // indirect github.com/bytedance/sonic/loader v0.2.0 // indirect github.com/caarlos0/env/v6 v6.10.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/charmbracelet/bubbletea v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // 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 @@ -70,8 +78,11 @@ require ( 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/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // 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 @@ -79,6 +90,9 @@ require ( 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/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/natefinch/lumberjack v2.0.0+incompatible // indirect github.com/panjf2000/ants/v2 v2.10.0 // indirect @@ -90,6 +104,7 @@ require ( 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/rivo/uniseg v0.4.7 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect diff --git a/go.sum b/go.sum index b3eb224..1698e14 100644 --- a/go.sum +++ b/go.sum @@ -106,6 +106,10 @@ github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQ github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.35.5/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -139,6 +143,18 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c= +github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -189,6 +205,8 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg= +github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= @@ -196,6 +214,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= @@ -572,6 +592,8 @@ github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-b github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lixh00/loki-client-go v1.0.1 h1:y/ePf/s66N77eikIujRS/QQAKvbMmPmesMxAuMuP8lM= github.com/lixh00/loki-client-go v1.0.1/go.mod h1:JSeu3fIBPjnmf5bBq6I8hvJlhYum2eLQEzwU149vyfQ= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= @@ -595,8 +617,12 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -638,6 +664,12 @@ github.com/mojocn/base64Captcha v1.3.6/go.mod h1:i5CtHvm+oMbj1UzEPXaA8IH/xHFZ3DG github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= @@ -758,6 +790,9 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qq github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= @@ -1108,6 +1143,7 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/http/api/login.go b/http/api/login.go index e3d9571..b6b1d1e 100644 --- a/http/api/login.go +++ b/http/api/login.go @@ -76,7 +76,7 @@ func (LoginApi) Login(c *gin.Context) { secret := component.JWT().GenerateSecret(p.Password, uuid.NewString(), time.Now().Local().String()) // 生成token - token, expireAt, err := component.JWT().GenerateToken(user.Id, secret) + token, expireAt, err := component.JWT().GenerateToken(user.Id, secret, "http") if err != nil { log.Errorf("用户[%s]生成token失败: %v", user.Account, err.Error()) response.R(c).FailedWithError("登陆失败!") diff --git a/http/api/setting.go b/http/api/setting.go index 636ee91..007f6cf 100644 --- a/http/api/setting.go +++ b/http/api/setting.go @@ -67,7 +67,7 @@ func (setting) Delete(c *gin.Context) { return } - if err := service.Setting().Model(&model.Setting{}).Where("code = ?", code).Delete(&model.Setting{}).Error; err != nil { + if err := service.Setting().Model(&model.Setting{}).Where("code NOT IN (?)", []string{"WG_SETTING", "WG_SERVER"}).Where("code = ?", code).Delete(&model.Setting{}).Error; err != nil { response.R(c).FailedWithError("删除失败") return } diff --git a/http/middleware/authorization.go b/http/middleware/authorization.go index dc225c6..1f9fa41 100644 --- a/http/middleware/authorization.go +++ b/http/middleware/authorization.go @@ -33,7 +33,7 @@ func Authorization() gin.HandlerFunc { return } - userClaims, err := component.JWT().ParseToken(token, hashPassword) + userClaims, err := component.JWT().ParseToken(token, hashPassword, "http") if err != nil { response.R(c).AuthorizationFailed("未登陆") c.Abort() @@ -70,7 +70,7 @@ func Authorization() gin.HandlerFunc { // 生成一个新token secret := component.JWT().GenerateSecret(user.Password, uuid.NewString(), time.Now().Local().String()) - tokenStr, _, err := component.JWT().GenerateToken(user.Id, secret, userClaims.ExpiresAt.Time, time.Now().Local()) + tokenStr, _, err := component.JWT().GenerateToken(user.Id, secret, "http", userClaims.ExpiresAt.Time, time.Now().Local()) if err != nil { response.R(c).AuthorizationFailed("校验失败") c.Abort() diff --git a/http/vo/setting.go b/http/vo/setting.go index c881e5b..dcdffc6 100644 --- a/http/vo/setting.go +++ b/http/vo/setting.go @@ -1,13 +1,18 @@ package vo -import "wireguard-ui/global/constant" +import ( + "wireguard-ui/global/constant" + "wireguard-ui/model" +) // SettingItem // @description: 设置单项 type SettingItem struct { - Code string `json:"code"` - Data string `json:"data"` - Describe string `json:"describe"` + Code string `json:"code"` + Data string `json:"data"` + Describe string `json:"describe"` + CreatedAt model.JsonTime `json:"createdAt"` + UpdatedAt model.JsonTime `json:"updatedAt"` } type Export struct { diff --git a/initialize/init.go b/initialize/init.go index 5060a2f..3dddb64 100644 --- a/initialize/init.go +++ b/initialize/init.go @@ -6,6 +6,7 @@ import ( "gitee.ltd/lxh/logger/log" "github.com/cowardmrx/go_aliyun_oss" "github.com/fsnotify/fsnotify" + "github.com/gin-gonic/gin" "github.com/glebarez/sqlite" "github.com/go-resty/resty/v2" "github.com/redis/go-redis/v9" @@ -15,6 +16,7 @@ import ( "gorm.io/driver/postgres" "gorm.io/gorm" gl "gorm.io/gorm/logger" + "os" "time" "wireguard-ui/config" "wireguard-ui/global/client" @@ -140,8 +142,14 @@ func initOSS() { // initLogger // @description: 初始化日志 func initLogger() { + + mode := logger.Dev + if os.Getenv("GIN_MODE") == gin.ReleaseMode { + mode = logger.Prod + } + logger.InitLogger(logger.LogConfig{ - Mode: logger.Dev, + Mode: mode, FileEnable: true, }) } diff --git a/service/setting.go b/service/setting.go index 68c978b..861fbfb 100644 --- a/service/setting.go +++ b/service/setting.go @@ -86,7 +86,7 @@ func (s setting) GetWGServerForConfig() (data *render_data.Server, err error) { // @return data // @return err func (s setting) GetAllSetting(blackList []string) (data []vo.SettingItem, err error) { - err = s.Model(&model.Setting{}).Select("code, data, describe").Where("code not in ?", blackList).Find(&data).Error + err = s.Model(&model.Setting{}).Select("code, data, describe,created_at,updated_at").Where("code not in ?", blackList).Find(&data).Error return } diff --git a/utils/qr_code.go b/utils/qr_code.go index 5521854..c17769b 100644 --- a/utils/qr_code.go +++ b/utils/qr_code.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "gitee.ltd/lxh/logger/log" "github.com/skip2/go-qrcode" + "image/color" ) type qrCode struct{} @@ -36,3 +37,45 @@ func (qr qrCode) GenerateQrCodeBase64(content []byte, size int) (imgStr string, imgStr = "data:image/png;base64," + base64.StdEncoding.EncodeToString(png) return } + +// GenerateQrCodeASCII +// @description: 生成二维码 +// @receiver qr +// @param content +// @param size +// @return imgStr +// @return err +func (qr qrCode) GenerateQrCodeASCII(content []byte) (str string, err error) { + q, err := qrcode.New(string(content), qrcode.Highest) + if err != nil { + log.Errorf("初始化二维码对象失败: %v", err.Error()) + return + } + + // 设置二维码的尺寸,这里是 16 的倍数 + size := 16 + img := q.Image(size) + + isBlock := func(c color.Color) bool { + r, g, b, _ := c.RGBA() + // 转换到 0-255 范围,并判断是否接近黑色 + return r < 5000 && g < 5000 && b < 5000 + } + + for y := 0; y < img.Bounds().Dy(); y += 16 { + for x := 0; x < img.Bounds().Dx(); x++ { + // 获取当前像素的颜色 + color := img.At(x, y) + + // 判断像素是否接近黑色,黑色输出为 '██',白色输出为空格 + if isBlock(color) { + str += "██" // 黑色模块,输出方块字符 + } else { + str += " " // 白色模块,输出空格 + } + } + str += "\n" + } + + return str, nil +} From 9674796f0a5bcc11f5bfd5aaa19947cda4e42f26 Mon Sep 17 00:00:00 2001 From: coward Date: Tue, 24 Dec 2024 16:37:45 +0800 Subject: [PATCH 2/2] =?UTF-8?q?:art:=E5=88=A0=E9=99=A4=E6=B2=A1=E5=BF=85?= =?UTF-8?q?=E8=A6=81=E7=9A=84=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- utils/qr_code.go | 43 ------------------------------------------- 1 file changed, 43 deletions(-) diff --git a/utils/qr_code.go b/utils/qr_code.go index c17769b..5521854 100644 --- a/utils/qr_code.go +++ b/utils/qr_code.go @@ -4,7 +4,6 @@ import ( "encoding/base64" "gitee.ltd/lxh/logger/log" "github.com/skip2/go-qrcode" - "image/color" ) type qrCode struct{} @@ -37,45 +36,3 @@ func (qr qrCode) GenerateQrCodeBase64(content []byte, size int) (imgStr string, imgStr = "data:image/png;base64," + base64.StdEncoding.EncodeToString(png) return } - -// GenerateQrCodeASCII -// @description: 生成二维码 -// @receiver qr -// @param content -// @param size -// @return imgStr -// @return err -func (qr qrCode) GenerateQrCodeASCII(content []byte) (str string, err error) { - q, err := qrcode.New(string(content), qrcode.Highest) - if err != nil { - log.Errorf("初始化二维码对象失败: %v", err.Error()) - return - } - - // 设置二维码的尺寸,这里是 16 的倍数 - size := 16 - img := q.Image(size) - - isBlock := func(c color.Color) bool { - r, g, b, _ := c.RGBA() - // 转换到 0-255 范围,并判断是否接近黑色 - return r < 5000 && g < 5000 && b < 5000 - } - - for y := 0; y < img.Bounds().Dy(); y += 16 { - for x := 0; x < img.Bounds().Dx(); x++ { - // 获取当前像素的颜色 - color := img.At(x, y) - - // 判断像素是否接近黑色,黑色输出为 '██',白色输出为空格 - if isBlock(color) { - str += "██" // 黑色模块,输出方块字符 - } else { - str += " " // 白色模块,输出空格 - } - } - str += "\n" - } - - return str, nil -}