├── .gitattributes ├── README.md └── src ├── api └── handlers.go ├── auth └── auth.go ├── build.sh ├── config └── config.go ├── database ├── commands.go ├── database.go ├── terminal.go ├── user.go └── whitelist.go ├── go.mod ├── go.sum ├── main.go ├── middleware └── middleware.go ├── models ├── terminal.go └── user.go ├── terminal ├── handler.go ├── session.go └── unix.go ├── utils └── utils.go └── web └── out ├── 404.html ├── _next └── static │ ├── TFz4HhDxhTU5FA-Rc0bx2 │ ├── _buildManifest.js │ └── _ssgManifest.js │ ├── chunks │ ├── 105.6436354f7a5025d4.js │ ├── 117-2f4890905037fa56.js │ ├── 130-657e507e7c0a9bfa.js │ ├── 223.f420fe603d72c60d.js │ ├── 253-95c9bbffcbc06f61.js │ ├── 30d07d85.9045c666bb43d9e1.js │ ├── 747-b7871a7d8b45f8b8.js │ ├── 846.958e4cd3e1795d69.js │ ├── app │ │ ├── _not-found │ │ │ └── page-6d6f02352e3a435e.js │ │ ├── layout-2494ccb28bcae9b7.js │ │ ├── login │ │ │ └── page-e8df8927cd5da1d8.js │ │ └── page-b5682bda19311bce.js │ ├── fd9d1056-dd504a17eef30e15.js │ ├── framework-f66176bb897dc684.js │ ├── main-3e39d0fa78a4151f.js │ ├── main-app-922bba6b3ffca5f3.js │ ├── pages │ │ ├── _app-72b849fbd24ac258.js │ │ └── _error-7ba65e1336b92748.js │ ├── polyfills-42372ed130431b0a.js │ └── webpack-843b083001d99fcc.js │ ├── css │ ├── a2d275681cc35a24.css │ └── ab3e4b0f5035257a.css │ └── media │ ├── 26a46d62cd723877-s.woff2 │ ├── 55c55f0601d81cf3-s.woff2 │ ├── 581909926a08bbc8-s.woff2 │ ├── 6d93bde91c0c2823-s.woff2 │ ├── 97e0cb1ae144a2a9-s.woff2 │ ├── a34f9d1faa5f3315-s.p.woff2 │ └── df0a9ae256c0569c-s.woff2 ├── index.html ├── index.txt ├── login.html ├── login.txt ├── placeholder-logo.png ├── placeholder-logo.svg ├── placeholder-user.jpg ├── placeholder.jpg └── placeholder.svg /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌐 GhostEye 2 | 3 |
4 | 5 | ![Version](https://img.shields.io/badge/版本-1.0.0-blue) 6 | ![License](https://img.shields.io/badge/许可证-MIT-green) 7 | ![Platform](https://img.shields.io/badge/平台-Linux%20-lightgrey) 8 | 9 |
10 | 11 | > GhostEye 是一个为渗透测试设计的专业Web Shell管理面板。 12 | 13 | 14 | ## 🔍 背景与优势 15 | 16 | 传统的ssh工具存在多种问题,此工具就是为了简化红队人员在渗透测试过程中的繁琐操作而设计: 17 | 18 | ### ⚠️ 传统工具的局限性 19 | 20 | - **🕒 网络延迟问题**:Tabby或直接SSH连接在高延迟网络环境下输入命令极其卡顿,影响操作效率 21 | - **📶 连接稳定性差**:FinalShell虽然有独立输入框,但在网络波动情况下依然容易掉线,导致反弹shell中断 22 | - **🔄 会话恢复繁琐**:虽然screen命令可以在掉线后恢复会话,但每次手动设置非常麻烦 23 | - **🧩 反弹Shell流程复杂**: 24 | - 反弹shell命令种类繁多,需要借助HackTools等工具生成 25 | - 监听端口需要在SSH工具中手动设置,然后输入到命令生成工具,再复制执行 26 | - 部分环境下shell命令无法直接反弹,需要通过`echo ""|base64 -d |bash`间接执行 27 | - **⌨️ 命令重复输入**:信息收集、权限提升、工具下载等常用命令需要反复手动输入,效率低下 28 | 29 | --- 30 | 31 | ## ✨ 特性 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
🚀 一键监听自动设置监听端口,无需手动配置
🔄 命令同步端口自动同步到反弹命令中,省去手动修改步骤
🔐 编码转换一键生成base64反弹命令,满足各种环境需求
⏱️ 持久会话后台保留shell会话,即使退出登录或重启浏览器也能立即恢复终端状态
📶 无惧网络波动专为不稳定网络环境设计,确保shell连接稳定可靠
📚 命令模板库保存和管理常用命令,如信息收集、提权、下载工具等,一键调用无需重复输入
59 | 60 | --- 61 | 62 | ## 📥 Build 63 | 64 | ```bash 65 | # 克隆仓库 66 | git clone https://github.com/s1null/GhostEye.git 67 | 68 | # 进入项目目录 69 | cd GhostEye/src 70 | 71 | # 构建项目 72 | ./build.sh 73 | ``` 74 | 75 | --- 76 | 77 | ## 🛠️ 使用方法 78 | 79 | GhostEye 支持多种命令行参数来自定义其行为: 80 | 81 | ``` 82 | ./ghosteye [选项] 83 | 84 | 选项: 85 | -p string 服务器监听端口 (默认 "8080") 86 | -user string 指定管理员用户名 87 | -pass string 指定管理员密码 88 | -U int 自动生成指定数量的随机用户 89 | -w string 白名单IP地址,多个IP用逗号分隔 90 | --show-users 显示所有用户账号信息 91 | ``` 92 | ### 🔡 rlwrap增强Shell体验 93 | 94 | GhostEye使用rlwrap nc进行监听,让你在使用反弹Shell时可以使用上下左右键而不会出现乱码: 95 | 96 | 安装和使用rlwrap 97 | 98 | ```bash 99 | # 在Debian/Ubuntu上安装rlwrap 100 | sudo apt-get install rlwrap 101 | 102 | # 在CentOS/RHEL上安装rlwrap 103 | sudo yum install rlwrap 104 | ``` 105 | 106 | 使用rlwrap接收反弹Shell: 107 | ```bash 108 | # 使用rlwrap启动nc监听 109 | rlwrap nc -lvnp [PORT] 110 | 111 | ``` 112 | 113 | rlwrap的优势: 114 | - 支持命令历史记录 (使用上下箭头键) 115 | - 支持命令行编辑 (使用左右箭头键) 116 | - 不会因为使用特殊键而导致shell出现乱码 117 | - 增强了交互式shell的可用性 118 | 119 | 120 | ### 📝 示例 121 | 122 | 点击展开使用示例 123 | 124 | ```bash 125 | # 使用指定端口启动服务 126 | ./ghosteye -p 9090 127 | 128 | # 指定管理员账号启动 129 | ./ghosteye -user admin -pass secure_password 130 | 131 | # 启动并限制只允许特定IP访问 132 | ./ghosteye -w 192.168.1.100,10.0.0.5 133 | 134 | # 生成5个随机用户并启动 135 | ./ghosteye -U 5 136 | 137 | # 显示所有已创建的用户账号信息 138 | ./ghosteye --show-users 139 | ``` 140 | 141 | 142 | ## 📋 使用示例 143 | 144 | GhostEye允许您保存常用命令作为模板,极大提高工作效率: 145 | 146 | ### 🆕 添加新命令模板 147 | - 在界面中点击"Add Command"按钮 148 | - 填写命令名称、具体命令内容、命令描述 149 | - 点击保存 150 | ![image](https://github.com/user-attachments/assets/bbddfdd8-97f4-4d20-bf6e-0fc3b588be8e) 151 | 152 | ### 🚀 快速调用 153 | - 在终端会话中只需点击已保存的命令即可自动填充到输入区 154 | - 可以先修改再执行,适应不同环境需求 155 | - 点击base64会自动将输入框的命令转换为`echo ""|base64 -d |bash` 156 | ![image](https://github.com/user-attachments/assets/9774520d-5e3e-40a6-8e9e-0dee4e5574c4) 157 | 158 | ### 🔄 反弹Shell命令示例 159 | ![image](https://github.com/user-attachments/assets/980efb2e-bff7-40d7-96b0-7d7d944e378f) 160 | 161 | 162 | 163 | --- 164 | 165 | ## 🔑 登录系统 166 | 167 | 启动后,可以通过浏览器访问服务: 168 | 169 | ``` 170 | http://localhost:8080 171 | ``` 172 | 173 | 如果没有指定管理员账号,系统会自动创建默认账号: 174 | - 用户名: `admin` 175 | - 密码: `随机生成在终端显示` 176 | 177 | 178 | --- 179 | 180 | ## ⚖️ 免责声明 181 | 182 | GhostEye 仅供合法的安全测试和教育目的使用。用户须遵守所有适用的法律法规,对因滥用本工具造成的任何后果自行承担全部责任。 183 | 184 | --- 185 | 186 |
187 | 188 |

Made with ❤️ for Penetration Testers

189 |

Copyright © 2025 s1null

190 | 191 |
192 | -------------------------------------------------------------------------------- /src/api/handlers.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | 8 | "ghosteye/auth" 9 | "ghosteye/database" 10 | "ghosteye/middleware" 11 | "ghosteye/models" 12 | "ghosteye/utils" 13 | "ghosteye/terminal" 14 | ) 15 | 16 | // StartHandler 用于接收启动指令 17 | func StartHandler(w http.ResponseWriter, r *http.Request) { 18 | userState := middleware.GetUserState(r) 19 | if userState == nil { 20 | w.WriteHeader(http.StatusInternalServerError) 21 | w.Write([]byte("Failed to get user state")) 22 | return 23 | } 24 | 25 | userState.Mutex.Lock() 26 | defer userState.Mutex.Unlock() 27 | 28 | // 获取命令参数 29 | cmd := r.URL.Query().Get("cmd") 30 | if cmd == "" { 31 | utils.WriteJSON(w, models.Response{Code: 1, Message: "Missing parameter: cmd"}) 32 | return 33 | } 34 | 35 | // 更新用户状态 36 | userState.IsRunning = true 37 | userState.Command = cmd 38 | 39 | // 返回成功响应 40 | utils.WriteJSON(w, models.Response{Code: 0, Message: "Command received", Data: map[string]string{"cmd": cmd}}) 41 | } 42 | 43 | // StopHandler 用于接收停止指令 44 | func StopHandler(w http.ResponseWriter, r *http.Request) { 45 | userState := middleware.GetUserState(r) 46 | if userState == nil { 47 | w.WriteHeader(http.StatusInternalServerError) 48 | w.Write([]byte("Failed to get user state")) 49 | return 50 | } 51 | 52 | userState.Mutex.Lock() 53 | defer userState.Mutex.Unlock() 54 | 55 | // 更新用户状态 56 | userState.IsRunning = false 57 | userState.Command = "" 58 | 59 | // 返回成功响应 60 | utils.WriteJSON(w, models.Response{Code: 0, Message: "Command stopped"}) 61 | } 62 | 63 | // StatusHandler 用于查询命令状态 64 | func StatusHandler(w http.ResponseWriter, r *http.Request) { 65 | userState := middleware.GetUserState(r) 66 | if userState == nil { 67 | w.WriteHeader(http.StatusInternalServerError) 68 | w.Write([]byte("Failed to get user state")) 69 | return 70 | } 71 | 72 | userState.Mutex.Lock() 73 | defer userState.Mutex.Unlock() 74 | 75 | // 返回状态 76 | utils.WriteJSON(w, models.Response{ 77 | Code: 0, 78 | Message: "Status retrieved", 79 | Data: map[string]interface{}{ 80 | "is_running": userState.IsRunning, 81 | "command": userState.Command, 82 | }, 83 | }) 84 | } 85 | 86 | // LoginHandler 处理用户登录 87 | func LoginHandler(w http.ResponseWriter, r *http.Request) { 88 | // 只接受POST请求 89 | if r.Method != "POST" { 90 | w.WriteHeader(http.StatusMethodNotAllowed) 91 | w.Write([]byte("Method not allowed")) 92 | return 93 | } 94 | 95 | // 解析请求体 96 | var authReq models.AuthRequest 97 | if err := json.NewDecoder(r.Body).Decode(&authReq); err != nil { 98 | utils.WriteJSON(w, models.Response{Code: 1, Message: "Invalid request format"}) 99 | return 100 | } 101 | 102 | // 验证用户名和密码 103 | if !database.ValidateUser(authReq.Username, authReq.Password) { 104 | utils.WriteJSON(w, models.Response{Code: 1, Message: "Invalid username or password"}) 105 | return 106 | } 107 | 108 | // 生成会话令牌 109 | token := auth.SaveSessionToken(authReq.Username) 110 | 111 | // 返回成功响应 112 | utils.WriteJSON(w, models.Response{ 113 | Code: 0, 114 | Message: "Login successful", 115 | Data: map[string]string{ 116 | "token": token, 117 | "username": authReq.Username, 118 | }, 119 | }) 120 | } 121 | 122 | // IndexHandler 处理根路径请求 123 | func IndexHandler(w http.ResponseWriter, r *http.Request) { 124 | // 如果请求的是根路径,重定向到web目录 125 | if r.URL.Path == "/" { 126 | http.ServeFile(w, r, "web/index.html") 127 | return 128 | } 129 | 130 | // 否则尝试提供静态文件 131 | http.ServeFile(w, r, "web"+r.URL.Path) 132 | } 133 | 134 | // GetUserCommandsHandler 获取用户命令 135 | func GetUserCommandsHandler(w http.ResponseWriter, r *http.Request) { 136 | username := middleware.GetUsernameFromContext(r) 137 | if username == "" { 138 | w.WriteHeader(http.StatusUnauthorized) 139 | w.Write([]byte("Unauthorized")) 140 | return 141 | } 142 | 143 | commands, err := database.GetUserCommands(username) 144 | if err != nil { 145 | log.Printf("Failed to get user commands: %v", err) 146 | utils.WriteJSON(w, models.Response{Code: 1, Message: "Failed to get user commands"}) 147 | return 148 | } 149 | 150 | utils.WriteJSON(w, models.Response{ 151 | Code: 0, 152 | Message: "Commands retrieved", 153 | Data: commands, 154 | }) 155 | } 156 | 157 | // AddUserCommandHandler 添加用户命令 158 | func AddUserCommandHandler(w http.ResponseWriter, r *http.Request) { 159 | username := middleware.GetUsernameFromContext(r) 160 | if username == "" { 161 | w.WriteHeader(http.StatusUnauthorized) 162 | w.Write([]byte("Unauthorized")) 163 | return 164 | } 165 | 166 | // 解析请求体 167 | var cmd struct { 168 | Name string `json:"name"` 169 | Command string `json:"command"` 170 | Description string `json:"description"` 171 | } 172 | if err := json.NewDecoder(r.Body).Decode(&cmd); err != nil { 173 | utils.WriteJSON(w, models.Response{Code: 1, Message: "Invalid request format"}) 174 | return 175 | } 176 | 177 | // 验证参数 178 | if cmd.Name == "" || cmd.Command == "" { 179 | utils.WriteJSON(w, models.Response{Code: 1, Message: "Name and command are required"}) 180 | return 181 | } 182 | 183 | // 添加命令 184 | err := database.AddUserCommand(username, cmd.Name, cmd.Command, cmd.Description) 185 | if err != nil { 186 | log.Printf("Failed to add user command: %v", err) 187 | utils.WriteJSON(w, models.Response{Code: 1, Message: err.Error()}) 188 | return 189 | } 190 | 191 | utils.WriteJSON(w, models.Response{ 192 | Code: 0, 193 | Message: "Command added", 194 | }) 195 | } 196 | 197 | // UpdateUserCommandHandler 更新用户命令 198 | func UpdateUserCommandHandler(w http.ResponseWriter, r *http.Request) { 199 | username := middleware.GetUsernameFromContext(r) 200 | if username == "" { 201 | w.WriteHeader(http.StatusUnauthorized) 202 | w.Write([]byte("Unauthorized")) 203 | return 204 | } 205 | 206 | // 解析请求体 207 | var cmd struct { 208 | Name string `json:"name"` 209 | Command string `json:"command"` 210 | Description string `json:"description"` 211 | } 212 | if err := json.NewDecoder(r.Body).Decode(&cmd); err != nil { 213 | utils.WriteJSON(w, models.Response{Code: 1, Message: "Invalid request format"}) 214 | return 215 | } 216 | 217 | // 验证参数 218 | if cmd.Name == "" || cmd.Command == "" { 219 | utils.WriteJSON(w, models.Response{Code: 1, Message: "Name and command are required"}) 220 | return 221 | } 222 | 223 | // 更新命令 224 | err := database.UpdateUserCommand(username, cmd.Name, cmd.Command, cmd.Description) 225 | if err != nil { 226 | log.Printf("Failed to update user command: %v", err) 227 | utils.WriteJSON(w, models.Response{Code: 1, Message: err.Error()}) 228 | return 229 | } 230 | 231 | utils.WriteJSON(w, models.Response{ 232 | Code: 0, 233 | Message: "Command updated", 234 | }) 235 | } 236 | 237 | // DeleteUserCommandHandler 删除用户命令 238 | func DeleteUserCommandHandler(w http.ResponseWriter, r *http.Request) { 239 | username := middleware.GetUsernameFromContext(r) 240 | if username == "" { 241 | w.WriteHeader(http.StatusUnauthorized) 242 | w.Write([]byte("Unauthorized")) 243 | return 244 | } 245 | 246 | // 获取命令名称 247 | name := r.URL.Query().Get("name") 248 | if name == "" { 249 | utils.WriteJSON(w, models.Response{Code: 1, Message: "Missing parameter: name"}) 250 | return 251 | } 252 | 253 | // 删除命令 254 | err := database.DeleteUserCommand(username, name) 255 | if err != nil { 256 | log.Printf("Failed to delete user command: %v", err) 257 | utils.WriteJSON(w, models.Response{Code: 1, Message: err.Error()}) 258 | return 259 | } 260 | 261 | utils.WriteJSON(w, models.Response{ 262 | Code: 0, 263 | Message: "Command deleted", 264 | }) 265 | } 266 | 267 | // ListTerminalSessionsHandler 列出终端会话 268 | func ListTerminalSessionsHandler(w http.ResponseWriter, r *http.Request) { 269 | username := middleware.GetUsernameFromContext(r) 270 | if username == "" { 271 | w.WriteHeader(http.StatusUnauthorized) 272 | w.Write([]byte("Unauthorized")) 273 | return 274 | } 275 | 276 | sessions, err := database.GetUserTerminalSessions(username) 277 | if err != nil { 278 | log.Printf("Failed to get user terminal sessions: %v", err) 279 | utils.WriteJSON(w, models.Response{Code: 1, Message: "Failed to get terminal sessions"}) 280 | return 281 | } 282 | 283 | utils.WriteJSON(w, models.Response{ 284 | Code: 0, 285 | Message: "Terminal sessions retrieved", 286 | Data: sessions, 287 | }) 288 | } 289 | 290 | // KillTerminalHandler 处理终止终端的请求 291 | func KillTerminalHandler(w http.ResponseWriter, r *http.Request) { 292 | username := middleware.GetUsernameFromContext(r) 293 | if username == "" { 294 | w.WriteHeader(http.StatusUnauthorized) 295 | w.Write([]byte("Unauthorized")) 296 | return 297 | } 298 | 299 | // 获取终端ID 300 | terminalID := r.URL.Query().Get("terminal_id") 301 | if terminalID == "" { 302 | utils.WriteJSON(w, models.Response{Code: 1, Message: "Missing parameter: terminal_id"}) 303 | return 304 | } 305 | 306 | // 终止终端会话 307 | err := terminal.KillTerminalSession(username, terminalID) 308 | if err != nil { 309 | log.Printf("Failed to terminate terminal session: %v", err) 310 | utils.WriteJSON(w, models.Response{Code: 1, Message: err.Error()}) 311 | return 312 | } 313 | 314 | utils.WriteJSON(w, models.Response{ 315 | Code: 0, 316 | Message: "Terminal session terminated", 317 | }) 318 | } 319 | -------------------------------------------------------------------------------- /src/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "log" 5 | "ghosteye/models" 6 | "ghosteye/utils" 7 | ) 8 | 9 | // SaveSessionToken 保存会话token 10 | func SaveSessionToken(username string) string { 11 | token := utils.GenerateToken() 12 | 13 | models.SessionTokensMux.Lock() 14 | models.SessionTokens[token] = username 15 | models.SessionTokensMux.Unlock() 16 | 17 | log.Printf("Created new session token for user %s", username) 18 | 19 | return token 20 | } 21 | 22 | // ValidateToken 验证会话token 23 | func ValidateToken(token string) (string, bool) { 24 | models.SessionTokensMux.Lock() 25 | defer models.SessionTokensMux.Unlock() 26 | 27 | username, exists := models.SessionTokens[token] 28 | return username, exists 29 | } 30 | 31 | // ValidateSessionToken 是 ValidateToken 的别名,保持代码兼容性 32 | func ValidateSessionToken(token string) (string, bool) { 33 | return ValidateToken(token) 34 | } 35 | 36 | // RemoveToken 删除会话token 37 | func RemoveToken(token string) { 38 | models.SessionTokensMux.Lock() 39 | defer models.SessionTokensMux.Unlock() 40 | 41 | delete(models.SessionTokens, token) 42 | } 43 | 44 | // GetUserState 获取用户状态 45 | func GetUserState(username string) *models.UserState { 46 | models.UserStatesMux.Lock() 47 | defer models.UserStatesMux.Unlock() 48 | 49 | userState, exists := models.UserStates[username] 50 | if !exists { 51 | userState = &models.UserState{ 52 | IsRunning: false, 53 | Command: "", 54 | } 55 | models.UserStates[username] = userState 56 | } 57 | 58 | return userState 59 | } -------------------------------------------------------------------------------- /src/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 设置颜色输出 4 | RED='\033[0;31m' 5 | GREEN='\033[0;32m' 6 | BLUE='\033[0;34m' 7 | YELLOW='\033[1;33m' 8 | NC='\033[0m' # 无颜色 9 | 10 | # 版本号 11 | VERSION="1.0.0" 12 | 13 | # 检查前端文件是否存在 14 | check_frontend_files() { 15 | echo -e "${BLUE}检查前端文件...${NC}" 16 | if [ ! -d "web/out" ]; then 17 | echo -e "${RED}错误: 前端文件目录 'web/out' 不存在!${NC}" 18 | echo -e "${YELLOW}提示: 请确保前端已构建并放置在正确位置,以便嵌入到二进制文件中。${NC}" 19 | return 1 20 | fi 21 | 22 | if [ ! -f "web/out/index.html" ]; then 23 | echo -e "${RED}错误: 前端主页文件 'web/out/index.html' 不存在!${NC}" 24 | echo -e "${YELLOW}提示: 请确保前端已正确构建。${NC}" 25 | return 1 26 | fi 27 | 28 | echo -e "${GREEN}前端文件检查通过。${NC}" 29 | return 0 30 | } 31 | 32 | # 创建输出目录 33 | mkdir -p build 34 | 35 | echo -e "${BLUE}开始编译 GhostEye v${VERSION}...${NC}" 36 | 37 | # 默认编译当前系统架构 38 | build_current() { 39 | echo -e "${GREEN}编译当前系统架构的二进制文件...${NC}" 40 | 41 | # 检查前端文件 42 | check_frontend_files || return 1 43 | 44 | # 执行go mod tidy整理依赖 45 | echo -e "${BLUE}执行 go mod tidy 整理依赖...${NC}" 46 | go mod tidy 47 | if [ $? -ne 0 ]; then 48 | echo -e "${RED}go mod tidy 执行失败!${NC}" 49 | return 1 50 | fi 51 | echo -e "${GREEN}依赖整理完成。${NC}" 52 | 53 | output_name="GhostEye" 54 | 55 | # 如果是Windows,添加.exe后缀 56 | if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then 57 | output_name="GhostEye.exe" 58 | fi 59 | 60 | echo -e "${BLUE}正在编译...${NC}" 61 | go build -o "build/$output_name" -ldflags "-s -w" . 62 | 63 | if [ $? -eq 0 ]; then 64 | echo -e "${GREEN}编译成功!二进制文件位于: build/$output_name${NC}" 65 | echo -e "${BLUE}提示: 直接运行 build/$output_name 即可启动服务,所有前端资源已嵌入。${NC}" 66 | else 67 | echo -e "${RED}编译失败!${NC}" 68 | exit 1 69 | fi 70 | } 71 | 72 | # 编译所有支持的平台 73 | build_all() { 74 | echo -e "${BLUE}开始为所有支持的平台编译GhostEye...${NC}" 75 | 76 | # 检查前端文件 77 | check_frontend_files || return 1 78 | 79 | # 执行go mod tidy整理依赖 80 | echo -e "${BLUE}执行 go mod tidy 整理依赖...${NC}" 81 | go mod tidy 82 | if [ $? -ne 0 ]; then 83 | echo -e "${RED}go mod tidy 执行失败!${NC}" 84 | return 1 85 | fi 86 | echo -e "${GREEN}依赖整理完成。${NC}" 87 | 88 | # 定义要编译的平台列表 89 | platforms=("linux/amd64" "linux/386" "linux/arm64" "linux/arm") 90 | 91 | for platform in "${platforms[@]}" 92 | do 93 | platform_split=(${platform//\// }) 94 | GOOS=${platform_split[0]} 95 | GOARCH=${platform_split[1]} 96 | 97 | output_name="ghosteye-$GOOS-$GOARCH" 98 | if [ $GOOS = "windows" ]; then 99 | output_name+=".exe" 100 | fi 101 | 102 | echo -e "${YELLOW}编译 $GOOS/$GOARCH...${NC}" 103 | env GOOS=$GOOS GOARCH=$GOARCH go build -o "build/$output_name" -ldflags "-s -w" . 104 | 105 | if [ $? -ne 0 ]; then 106 | echo -e "${RED}编译 $GOOS/$GOARCH 失败${NC}" 107 | else 108 | echo -e "${GREEN}编译 $GOOS/$GOARCH 成功${NC}" 109 | fi 110 | done 111 | 112 | echo -e "${GREEN}所有平台编译完成!${NC}" 113 | echo -e "${BLUE}提示: 所有二进制文件已包含嵌入式前端资源,可直接运行。${NC}" 114 | } 115 | 116 | # 清理编译文件 117 | clean() { 118 | echo -e "${BLUE}清理编译文件...${NC}" 119 | rm -rf build 120 | echo -e "${GREEN}清理完成${NC}" 121 | } 122 | 123 | # 显示帮助信息 124 | show_help() { 125 | echo "GhostEye 编译脚本 v${VERSION}" 126 | echo "用法: ./build.sh [选项]" 127 | echo "" 128 | echo "选项:" 129 | echo " -c, --current 仅编译当前系统架构的二进制文件" 130 | echo " -a, --all 编译所有支持的平台" 131 | echo " --clean 清理编译文件" 132 | echo " -h, --help 显示此帮助信息" 133 | echo "" 134 | echo "示例:" 135 | echo " ./build.sh # 默认编译当前系统架构" 136 | echo " ./build.sh --all # 编译所有支持的平台" 137 | echo " ./build.sh --clean # 清理编译文件" 138 | } 139 | 140 | # 解析命令行参数 141 | if [[ $# -eq 0 ]]; then 142 | build_current 143 | else 144 | case "$1" in 145 | -c|--current) 146 | build_current 147 | ;; 148 | -a|--all) 149 | build_all 150 | ;; 151 | --clean) 152 | clean 153 | ;; 154 | -h|--help) 155 | show_help 156 | ;; 157 | *) 158 | echo -e "${RED}未知选项: $1${NC}" 159 | show_help 160 | exit 1 161 | ;; 162 | esac 163 | fi 164 | 165 | exit 0 166 | -------------------------------------------------------------------------------- /src/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Config 存储应用程序的配置 4 | type Config struct { 5 | ServerPort string 6 | Username string 7 | Password string 8 | RandomUsers int 9 | WhitelistIPs string 10 | ShowUsers bool 11 | } 12 | 13 | // 全局配置实例 14 | var AppConfig Config 15 | 16 | // Initialize 初始化应用程序配置 17 | func Initialize(serverPort, username, password string, randomUsers int, whitelistIPs string, showUsers bool) { 18 | AppConfig = Config{ 19 | ServerPort: serverPort, 20 | Username: username, 21 | Password: password, 22 | RandomUsers: randomUsers, 23 | WhitelistIPs: whitelistIPs, 24 | ShowUsers: showUsers, 25 | } 26 | } 27 | 28 | // GetServerPort 获取服务器端口 29 | func GetServerPort() string { 30 | return AppConfig.ServerPort 31 | } 32 | 33 | // GetUsername 获取管理员用户名 34 | func GetUsername() string { 35 | return AppConfig.Username 36 | } 37 | 38 | // GetPassword 获取管理员密码 39 | func GetPassword() string { 40 | return AppConfig.Password 41 | } 42 | 43 | // GetRandomUsers 获取随机用户数量 44 | func GetRandomUsers() int { 45 | return AppConfig.RandomUsers 46 | } 47 | 48 | // GetWhitelistIPs 获取白名单IP地址 49 | func GetWhitelistIPs() string { 50 | return AppConfig.WhitelistIPs 51 | } 52 | 53 | // ShouldShowUsers 是否显示所有用户 54 | func ShouldShowUsers() bool { 55 | return AppConfig.ShowUsers 56 | } -------------------------------------------------------------------------------- /src/database/commands.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | ) 7 | 8 | // AddUserCommand 添加用户命令 9 | func AddUserCommand(username, name, command, description string) error { 10 | _, err := db.Exec( 11 | "INSERT INTO user_commands (username, name, command, description) VALUES (?, ?, ?, ?)", 12 | username, name, command, description, 13 | ) 14 | if err != nil { 15 | return fmt.Errorf("Failed to add user command: %v", err) 16 | } 17 | 18 | log.Printf("Command %s for user %s added successfully", name, username) 19 | return nil 20 | } 21 | 22 | // UpdateUserCommand 更新用户命令 23 | func UpdateUserCommand(username, name, command, description string) error { 24 | result, err := db.Exec( 25 | "UPDATE user_commands SET command = ?, description = ? WHERE username = ? AND name = ?", 26 | command, description, username, name, 27 | ) 28 | if err != nil { 29 | return fmt.Errorf("Failed to update user command: %v", err) 30 | } 31 | 32 | rowsAffected, err := result.RowsAffected() 33 | if err != nil { 34 | return fmt.Errorf("Failed to get affected rows: %v", err) 35 | } 36 | 37 | if rowsAffected == 0 { 38 | return fmt.Errorf("Command %s does not exist or does not belong to user %s", name, username) 39 | } 40 | 41 | log.Printf("Command %s for user %s updated successfully", name, username) 42 | return nil 43 | } 44 | 45 | // DeleteUserCommand 删除用户命令 46 | func DeleteUserCommand(username, name string) error { 47 | result, err := db.Exec( 48 | "DELETE FROM user_commands WHERE username = ? AND name = ?", 49 | username, name, 50 | ) 51 | if err != nil { 52 | return fmt.Errorf("Failed to delete user command: %v", err) 53 | } 54 | 55 | rowsAffected, err := result.RowsAffected() 56 | if err != nil { 57 | return fmt.Errorf("Failed to get affected rows: %v", err) 58 | } 59 | 60 | if rowsAffected == 0 { 61 | return fmt.Errorf("Command %s does not exist or does not belong to user %s", name, username) 62 | } 63 | 64 | log.Printf("Command %s for user %s deleted successfully", name, username) 65 | return nil 66 | } 67 | 68 | // GetUserCommands 获取用户命令 69 | func GetUserCommands(username string) ([]map[string]interface{}, error) { 70 | rows, err := db.Query( 71 | "SELECT id, name, command, description, created_at FROM user_commands WHERE username = ? ORDER BY name", 72 | username, 73 | ) 74 | if err != nil { 75 | return nil, fmt.Errorf("Failed to query user commands: %v", err) 76 | } 77 | defer rows.Close() 78 | 79 | commands := make([]map[string]interface{}, 0) 80 | for rows.Next() { 81 | var id int 82 | var name, command, description, createdAt string 83 | if err := rows.Scan(&id, &name, &command, &description, &createdAt); err != nil { 84 | return nil, fmt.Errorf("Failed to scan user command data: %v", err) 85 | } 86 | 87 | commands = append(commands, map[string]interface{}{ 88 | "id": id, 89 | "name": name, 90 | "command": command, 91 | "description": description, 92 | "created_at": createdAt, 93 | }) 94 | } 95 | 96 | return commands, nil 97 | } -------------------------------------------------------------------------------- /src/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | 8 | _ "modernc.org/sqlite" 9 | ) 10 | 11 | // 全局数据库连接 12 | var db *sql.DB 13 | 14 | // InitDatabase 初始化数据库 15 | func InitDatabase() error { 16 | var err error 17 | // 打开SQLite数据库连接 18 | db, err = sql.Open("sqlite", "./ghosteye.db") 19 | if err != nil { 20 | return fmt.Errorf("Failed to open database: %v", err) 21 | } 22 | 23 | // 创建用户表 24 | _, err = db.Exec(` 25 | CREATE TABLE IF NOT EXISTS users ( 26 | id INTEGER PRIMARY KEY AUTOINCREMENT, 27 | username TEXT UNIQUE NOT NULL, 28 | password TEXT NOT NULL, 29 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 30 | ) 31 | `) 32 | if err != nil { 33 | return fmt.Errorf("Failed to create users table: %v", err) 34 | } 35 | 36 | // 创建IP白名单表 37 | _, err = db.Exec(` 38 | CREATE TABLE IF NOT EXISTS ip_whitelist ( 39 | id INTEGER PRIMARY KEY AUTOINCREMENT, 40 | ip TEXT UNIQUE NOT NULL, 41 | description TEXT, 42 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 43 | ) 44 | `) 45 | if err != nil { 46 | return fmt.Errorf("Failed to create IP whitelist table: %v", err) 47 | } 48 | 49 | // 创建用户命令表 50 | _, err = db.Exec(` 51 | CREATE TABLE IF NOT EXISTS user_commands ( 52 | id INTEGER PRIMARY KEY AUTOINCREMENT, 53 | username TEXT NOT NULL, 54 | name TEXT NOT NULL, 55 | command TEXT NOT NULL, 56 | description TEXT, 57 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 58 | UNIQUE(username, name) 59 | ) 60 | `) 61 | if err != nil { 62 | return fmt.Errorf("Failed to create user commands table: %v", err) 63 | } 64 | 65 | // 创建终端会话表,用于持久化终端会话 66 | _, err = db.Exec(` 67 | CREATE TABLE IF NOT EXISTS terminal_sessions ( 68 | id INTEGER PRIMARY KEY AUTOINCREMENT, 69 | terminal_id TEXT NOT NULL, 70 | username TEXT NOT NULL, 71 | buffer BLOB, 72 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 73 | last_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 74 | active INTEGER DEFAULT 1, 75 | UNIQUE(username, terminal_id) 76 | ) 77 | `) 78 | if err != nil { 79 | return fmt.Errorf("Failed to create terminal sessions table: %v", err) 80 | } 81 | 82 | log.Println("Database initialized successfully") 83 | return nil 84 | } 85 | 86 | // CloseDatabase 关闭数据库连接 87 | func CloseDatabase() { 88 | if db != nil { 89 | db.Close() 90 | log.Println("Database connection closed") 91 | } 92 | } 93 | 94 | // GetDB 获取数据库连接 95 | func GetDB() *sql.DB { 96 | return db 97 | } -------------------------------------------------------------------------------- /src/database/terminal.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | ) 8 | 9 | // SaveTerminalSessionToDB 保存终端会话到数据库 10 | func SaveTerminalSessionToDB(username, terminalID string, buffer []byte) error { 11 | // 检查会话是否存在 12 | var count int 13 | err := db.QueryRow( 14 | "SELECT COUNT(*) FROM terminal_sessions WHERE username = ? AND terminal_id = ?", 15 | username, terminalID, 16 | ).Scan(&count) 17 | if err != nil { 18 | return fmt.Errorf("Failed to check terminal session: %v", err) 19 | } 20 | 21 | if count > 0 { 22 | // 更新现有会话 23 | _, err = db.Exec( 24 | "UPDATE terminal_sessions SET buffer = ?, last_active = CURRENT_TIMESTAMP, active = 1 WHERE username = ? AND terminal_id = ?", 25 | buffer, username, terminalID, 26 | ) 27 | if err != nil { 28 | return fmt.Errorf("Failed to update terminal session: %v", err) 29 | } 30 | } else { 31 | // 创建新会话 32 | _, err = db.Exec( 33 | "INSERT INTO terminal_sessions (username, terminal_id, buffer, active) VALUES (?, ?, ?, 1)", 34 | username, terminalID, buffer, 35 | ) 36 | if err != nil { 37 | return fmt.Errorf("Failed to create terminal session: %v", err) 38 | } 39 | } 40 | 41 | return nil 42 | } 43 | 44 | // LoadTerminalSessionFromDB 从数据库加载终端会话 45 | func LoadTerminalSessionFromDB(username, terminalID string) ([]byte, bool, error) { 46 | var buffer []byte 47 | var active int 48 | err := db.QueryRow( 49 | "SELECT buffer, active FROM terminal_sessions WHERE username = ? AND terminal_id = ?", 50 | username, terminalID, 51 | ).Scan(&buffer, &active) 52 | if err != nil { 53 | if err.Error() == "sql: no rows in result set" { 54 | return nil, false, nil 55 | } 56 | return nil, false, fmt.Errorf("Failed to load terminal session: %v", err) 57 | } 58 | 59 | return buffer, active == 1, nil 60 | } 61 | 62 | // SetTerminalSessionActive 设置终端会话活跃状态 63 | func SetTerminalSessionActive(username, terminalID string, active bool) error { 64 | activeValue := 0 65 | if active { 66 | activeValue = 1 67 | } 68 | 69 | _, err := db.Exec( 70 | "UPDATE terminal_sessions SET active = ?, last_active = CURRENT_TIMESTAMP WHERE username = ? AND terminal_id = ?", 71 | activeValue, username, terminalID, 72 | ) 73 | if err != nil { 74 | return fmt.Errorf("Failed to set terminal session active status: %v", err) 75 | } 76 | 77 | return nil 78 | } 79 | 80 | // GetUserTerminalSessions 获取用户的终端会话 81 | func GetUserTerminalSessions(username string) ([]map[string]interface{}, error) { 82 | rows, err := db.Query( 83 | "SELECT terminal_id, created_at, last_active, active FROM terminal_sessions WHERE username = ? ORDER BY last_active DESC", 84 | username, 85 | ) 86 | if err != nil { 87 | return nil, fmt.Errorf("Failed to query user terminal sessions: %v", err) 88 | } 89 | defer rows.Close() 90 | 91 | sessions := make([]map[string]interface{}, 0) 92 | for rows.Next() { 93 | var terminalID, createdAt, lastActive string 94 | var active int 95 | if err := rows.Scan(&terminalID, &createdAt, &lastActive, &active); err != nil { 96 | return nil, fmt.Errorf("Failed to scan user terminal session data: %v", err) 97 | } 98 | 99 | // 解析时间 100 | lastActiveTime, _ := time.Parse("2006-01-02 15:04:05", lastActive) 101 | createdAtTime, _ := time.Parse("2006-01-02 15:04:05", createdAt) 102 | 103 | sessions = append(sessions, map[string]interface{}{ 104 | "terminal_id": terminalID, 105 | "created_at": createdAt, 106 | "last_active": lastActive, 107 | "active": active == 1, 108 | "age": time.Since(lastActiveTime).String(), 109 | "duration": lastActiveTime.Sub(createdAtTime).String(), 110 | }) 111 | } 112 | 113 | return sessions, nil 114 | } 115 | 116 | // CleanupOldTerminalSessions 清理旧的终端会话 117 | func CleanupOldTerminalSessions(days int) error { 118 | result, err := db.Exec( 119 | "DELETE FROM terminal_sessions WHERE active = 0 AND julianday('now') - julianday(last_active) > ?", 120 | days, 121 | ) 122 | if err != nil { 123 | return fmt.Errorf("Failed to clean up old terminal sessions: %v", err) 124 | } 125 | 126 | rowsAffected, _ := result.RowsAffected() 127 | log.Printf("Cleaned up %d terminal sessions inactive for more than %d days", rowsAffected, days) 128 | 129 | return nil 130 | } 131 | 132 | // DeleteTerminalSessionFromDB 从数据库中完全删除终端会话 133 | func DeleteTerminalSessionFromDB(username, terminalID string) error { 134 | result, err := db.Exec( 135 | "DELETE FROM terminal_sessions WHERE username = ? AND terminal_id = ?", 136 | username, terminalID, 137 | ) 138 | if err != nil { 139 | return fmt.Errorf("Failed to delete terminal session from database: %v", err) 140 | } 141 | 142 | rowsAffected, _ := result.RowsAffected() 143 | if rowsAffected == 0 { 144 | log.Printf("No terminal session found to delete: %s", terminalID) 145 | return nil 146 | } 147 | 148 | log.Printf("Terminal session %s has been deleted from database", terminalID) 149 | return nil 150 | } 151 | -------------------------------------------------------------------------------- /src/database/user.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math/rand" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // AddUser 添加用户 12 | func AddUser(username, password string) error { 13 | _, err := db.Exec("INSERT INTO users (username, password) VALUES (?, ?)", username, password) 14 | if err != nil { 15 | if strings.Contains(err.Error(), "UNIQUE constraint failed") { 16 | return fmt.Errorf("Username %s already exists", username) 17 | } 18 | return fmt.Errorf("Failed to add user: %v", err) 19 | } 20 | log.Printf("User %s added successfully", username) 21 | return nil 22 | } 23 | 24 | // ValidateUser 验证用户 25 | func ValidateUser(username, password string) bool { 26 | var storedPassword string 27 | err := db.QueryRow("SELECT password FROM users WHERE username = ?", username).Scan(&storedPassword) 28 | if err != nil { 29 | log.Printf("Failed to query user: %v", err) 30 | return false 31 | } 32 | 33 | return password == storedPassword 34 | } 35 | 36 | // GenerateStrongPassword 生成强密码 37 | func GenerateStrongPassword(length int) string { 38 | if length < 8 { 39 | length = 8 // 最小长度为8 40 | } 41 | 42 | // 字符集 43 | lowercase := "abcdefghijklmnopqrstuvwxyz" 44 | uppercase := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 45 | numbers := "0123456789" 46 | symbols := "!@#$%^&*()-_=+[]{}|;:,.<>?" 47 | 48 | // 确保至少包含每种字符 49 | password := make([]byte, length) 50 | 51 | // 随机数生成器 52 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 53 | 54 | // 确保至少有一个小写字母 55 | password[0] = lowercase[r.Intn(len(lowercase))] 56 | 57 | // 确保至少有一个大写字母 58 | password[1] = uppercase[r.Intn(len(uppercase))] 59 | 60 | // 确保至少有一个数字 61 | password[2] = numbers[r.Intn(len(numbers))] 62 | 63 | // 确保至少有一个特殊符号 64 | password[3] = symbols[r.Intn(len(symbols))] 65 | 66 | // 填充剩余字符 67 | allChars := lowercase + uppercase + numbers + symbols 68 | for i := 4; i < length; i++ { 69 | password[i] = allChars[r.Intn(len(allChars))] 70 | } 71 | 72 | // 打乱顺序 73 | for i := range password { 74 | j := r.Intn(i + 1) 75 | password[i], password[j] = password[j], password[i] 76 | } 77 | 78 | return string(password) 79 | } 80 | 81 | // GenerateRandomUsername 生成随机用户名 82 | func GenerateRandomUsername(prefix string, length int) string { 83 | if length < 4 { 84 | length = 4 // 最小长度为4 85 | } 86 | 87 | // 字符集 88 | lowercase := "abcdefghijklmnopqrstuvwxyz" 89 | uppercase := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 90 | numbers := "0123456789" 91 | 92 | // 随机数生成器 93 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 94 | 95 | // 生成随机用户名 96 | username := make([]byte, length) 97 | 98 | // 确保至少有一个小写字母 99 | username[0] = lowercase[r.Intn(len(lowercase))] 100 | 101 | // 确保至少有一个大写字母 102 | username[1] = uppercase[r.Intn(len(uppercase))] 103 | 104 | // 确保至少有一个数字 105 | username[2] = numbers[r.Intn(len(numbers))] 106 | 107 | // 填充剩余字符 108 | allChars := lowercase + uppercase + numbers 109 | for i := 3; i < length; i++ { 110 | username[i] = allChars[r.Intn(len(allChars))] 111 | } 112 | 113 | // 打乱顺序 114 | for i := range username { 115 | j := r.Intn(i + 1) 116 | username[i], username[j] = username[j], username[i] 117 | } 118 | 119 | // 添加前缀 120 | if prefix != "" { 121 | return prefix + string(username) 122 | } 123 | 124 | return string(username) 125 | } 126 | 127 | // GenerateRandomUsers 生成随机用户 128 | func GenerateRandomUsers(count int) []map[string]string { 129 | users := make([]map[string]string, 0, count) 130 | 131 | for i := 0; i < count; i++ { 132 | username := GenerateRandomUsername("user_", 16) 133 | password := GenerateStrongPassword(16) 134 | 135 | err := AddUser(username, password) 136 | if err != nil { 137 | log.Printf("Failed to generate random user: %v", err) 138 | continue 139 | } 140 | 141 | users = append(users, map[string]string{ 142 | "username": username, 143 | "password": password, 144 | }) 145 | } 146 | 147 | return users 148 | } 149 | 150 | // GetAllUsers 获取所有用户 151 | func GetAllUsers() ([]map[string]string, error) { 152 | rows, err := db.Query("SELECT username, password FROM users") 153 | if err != nil { 154 | return nil, fmt.Errorf("Failed to query users: %v", err) 155 | } 156 | defer rows.Close() 157 | 158 | users := make([]map[string]string, 0) 159 | for rows.Next() { 160 | var username, password string 161 | if err := rows.Scan(&username, &password); err != nil { 162 | return nil, fmt.Errorf("Failed to scan user data: %v", err) 163 | } 164 | 165 | users = append(users, map[string]string{ 166 | "username": username, 167 | "password": password, 168 | }) 169 | } 170 | 171 | return users, nil 172 | } -------------------------------------------------------------------------------- /src/database/whitelist.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | ) 7 | 8 | // IsIPInWhitelist 检查IP是否在白名单中 9 | func IsIPInWhitelist(ip string) bool { 10 | // 如果白名单为空,允许所有IP 11 | var count int 12 | err := db.QueryRow("SELECT COUNT(*) FROM ip_whitelist").Scan(&count) 13 | if err != nil { 14 | log.Printf("Failed to query whitelist count: %v", err) 15 | return false 16 | } 17 | 18 | if count == 0 { 19 | return true 20 | } 21 | 22 | // 检查IP是否在白名单中 23 | var exists int 24 | err = db.QueryRow("SELECT COUNT(*) FROM ip_whitelist WHERE ip = ?", ip).Scan(&exists) 25 | if err != nil { 26 | log.Printf("Failed to query IP whitelist: %v", err) 27 | return false 28 | } 29 | 30 | return exists > 0 31 | } 32 | 33 | // AddIPToWhitelist 添加IP到白名单 34 | func AddIPToWhitelist(ip, description string) error { 35 | _, err := db.Exec("INSERT INTO ip_whitelist (ip, description) VALUES (?, ?)", ip, description) 36 | if err != nil { 37 | if err.Error() == "UNIQUE constraint failed: ip_whitelist.ip" { 38 | // 更新描述 39 | _, err = db.Exec("UPDATE ip_whitelist SET description = ? WHERE ip = ?", description, ip) 40 | if err != nil { 41 | return fmt.Errorf("Failed to update IP whitelist description: %v", err) 42 | } 43 | log.Printf("IP %s is already in the whitelist, description updated", ip) 44 | return nil 45 | } 46 | return fmt.Errorf("Failed to add IP to whitelist: %v", err) 47 | } 48 | log.Printf("IP %s has been added to the whitelist", ip) 49 | return nil 50 | } 51 | 52 | // RemoveIPFromWhitelist 从白名单中移除IP 53 | func RemoveIPFromWhitelist(ip string) error { 54 | result, err := db.Exec("DELETE FROM ip_whitelist WHERE ip = ?", ip) 55 | if err != nil { 56 | return fmt.Errorf("Failed to remove IP from whitelist: %v", err) 57 | } 58 | 59 | rowsAffected, err := result.RowsAffected() 60 | if err != nil { 61 | return fmt.Errorf("Failed to get affected rows: %v", err) 62 | } 63 | 64 | if rowsAffected == 0 { 65 | return fmt.Errorf("IP %s is not in the whitelist", ip) 66 | } 67 | 68 | log.Printf("IP %s has been removed from the whitelist", ip) 69 | return nil 70 | } 71 | 72 | // GetAllWhitelistIPs 获取所有白名单IP 73 | func GetAllWhitelistIPs() ([]map[string]string, error) { 74 | rows, err := db.Query("SELECT ip, description, created_at FROM ip_whitelist") 75 | if err != nil { 76 | return nil, fmt.Errorf("Failed to query whitelist IPs: %v", err) 77 | } 78 | defer rows.Close() 79 | 80 | ips := make([]map[string]string, 0) 81 | for rows.Next() { 82 | var ip, description, createdAt string 83 | if err := rows.Scan(&ip, &description, &createdAt); err != nil { 84 | return nil, fmt.Errorf("Failed to scan whitelist IP data: %v", err) 85 | } 86 | 87 | ips = append(ips, map[string]string{ 88 | "ip": ip, 89 | "description": description, 90 | "created_at": createdAt, 91 | }) 92 | } 93 | 94 | return ips, nil 95 | } -------------------------------------------------------------------------------- /src/go.mod: -------------------------------------------------------------------------------- 1 | module ghosteye 2 | 3 | go 1.23.6 4 | 5 | require ( 6 | github.com/creack/pty v1.1.24 7 | github.com/gorilla/websocket v1.5.3 8 | modernc.org/sqlite v1.28.0 9 | ) 10 | 11 | require ( 12 | github.com/dustin/go-humanize v1.0.1 // indirect 13 | github.com/google/uuid v1.3.0 // indirect 14 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 15 | github.com/mattn/go-isatty v0.0.16 // indirect 16 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 17 | golang.org/x/mod v0.3.0 // indirect 18 | golang.org/x/sys v0.9.0 // indirect 19 | golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 // indirect 20 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 21 | lukechampine.com/uint128 v1.2.0 // indirect 22 | modernc.org/cc/v3 v3.40.0 // indirect 23 | modernc.org/ccgo/v3 v3.16.13 // indirect 24 | modernc.org/libc v1.29.0 // indirect 25 | modernc.org/mathutil v1.6.0 // indirect 26 | modernc.org/memory v1.7.2 // indirect 27 | modernc.org/opt v0.1.3 // indirect 28 | modernc.org/strutil v1.1.3 // indirect 29 | modernc.org/token v1.0.1 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /src/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= 2 | github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 3 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 4 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 5 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 6 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 7 | github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= 8 | github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= 9 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 10 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 11 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 12 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 13 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 14 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 15 | github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= 16 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 17 | github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= 18 | github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 22 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 23 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 24 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 25 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 26 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 27 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 28 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 29 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 30 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 31 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 32 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 33 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 34 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 35 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 36 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 37 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= 39 | golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 41 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 42 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 43 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 44 | golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 h1:M8tBwCtWD/cZV9DZpFYRUgaymAYAr+aIUTWzDaM3uPs= 45 | golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 46 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 47 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 48 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 49 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 50 | lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= 51 | lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= 52 | modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= 53 | modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= 54 | modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= 55 | modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= 56 | modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= 57 | modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= 58 | modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= 59 | modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= 60 | modernc.org/libc v1.29.0 h1:tTFRFq69YKCF2QyGNuRUQxKBm1uZZLubf6Cjh/pVHXs= 61 | modernc.org/libc v1.29.0/go.mod h1:DaG/4Q3LRRdqpiLyP0C2m1B8ZMGkQ+cCgOIjEtQlYhQ= 62 | modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= 63 | modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= 64 | modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= 65 | modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= 66 | modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= 67 | modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= 68 | modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= 69 | modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= 70 | modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= 71 | modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= 72 | modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= 73 | modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c= 74 | modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= 75 | modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 76 | modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= 77 | modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE= 78 | -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "flag" 6 | "fmt" 7 | "io/fs" 8 | "log" 9 | "net/http" 10 | "os" 11 | "os/signal" 12 | "strings" 13 | "syscall" 14 | 15 | "ghosteye/api" 16 | "ghosteye/config" 17 | "ghosteye/database" 18 | "ghosteye/middleware" 19 | "ghosteye/terminal" 20 | ) 21 | 22 | //go:embed web/out/* 23 | var webContent embed.FS 24 | 25 | // Color constants for terminal output 26 | const ( 27 | colorReset = "\033[0m" 28 | colorRed = "\033[31m" 29 | colorGreen = "\033[32m" 30 | colorYellow = "\033[33m" 31 | colorBlue = "\033[34m" 32 | colorPurple = "\033[35m" 33 | colorCyan = "\033[36m" 34 | ) 35 | 36 | func main() { 37 | // 解析命令行参数 38 | serverPort := flag.String("p", "8080", "Server listening port") 39 | username := flag.String("user", "", "Specify admin username") 40 | password := flag.String("pass", "", "Specify admin password") 41 | randomUsers := flag.Int("U", 0, "Auto-generate specified number of random users") 42 | whitelistIPs := flag.String("w", "", "Whitelist IP addresses, separate multiple IPs with commas") 43 | showUsers := flag.Bool("show-users", false, "Show all user account information") 44 | flag.Parse() 45 | 46 | // 初始化配置 47 | config.Initialize(*serverPort, *username, *password, *randomUsers, *whitelistIPs, *showUsers) 48 | 49 | // 初始化数据库 50 | if err := database.InitDatabase(); err != nil { 51 | log.Fatalf("Database initialization failed: %v", err) 52 | } 53 | defer database.CloseDatabase() 54 | 55 | // 处理白名单IP 56 | if *whitelistIPs != "" { 57 | ips := strings.Split(*whitelistIPs, ",") 58 | for _, ip := range ips { 59 | ip = strings.TrimSpace(ip) 60 | if ip != "" { 61 | if err := database.AddIPToWhitelist(ip, "Added through command line parameters"); err != nil { 62 | log.Printf("Failed to add IP to whitelist: %v", err) 63 | } 64 | } 65 | } 66 | } 67 | 68 | // 处理用户账号 69 | if *username != "" && *password != "" { 70 | // 添加指定的管理员账号 71 | if err := database.AddUser(*username, *password); err != nil { 72 | log.Printf("Failed to add admin account: %v", err) 73 | } else { 74 | log.Printf("Admin account %s has been added", *username) 75 | fmt.Printf("Admin account: %s%s%s, Password: %s%s%s\n", 76 | colorGreen, *username, colorReset, 77 | colorYellow, *password, colorReset) 78 | } 79 | } 80 | 81 | // 生成随机用户 82 | if *randomUsers > 0 { 83 | users := database.GenerateRandomUsers(*randomUsers) 84 | log.Printf("Generated %d random users:", len(users)) 85 | fmt.Printf("%sGenerated %d random users:%s\n", colorBlue, len(users), colorReset) 86 | for _, user := range users { 87 | fmt.Printf("Username: %s%s%s, Password: %s%s%s\n", 88 | colorGreen, user["username"], colorReset, 89 | colorYellow, user["password"], colorReset) 90 | } 91 | } 92 | 93 | // 显示所有用户 94 | if *showUsers { 95 | users, err := database.GetAllUsers() 96 | if err != nil { 97 | log.Printf("Failed to get user list: %v", err) 98 | } else { 99 | log.Printf("There are %d users in the system:", len(users)) 100 | fmt.Printf("%sThere are %d users in the system:%s\n", colorPurple, len(users), colorReset) 101 | for _, user := range users { 102 | fmt.Printf("Username: %s%s%s, Password: %s%s%s\n", 103 | colorGreen, user["username"], colorReset, 104 | colorYellow, user["password"], colorReset) 105 | } 106 | } 107 | } 108 | 109 | // 如果没有任何用户,添加默认管理员账号 110 | users, _ := database.GetAllUsers() 111 | if len(users) == 0 { 112 | defaultUsername := "admin" 113 | defaultPassword := database.GenerateStrongPassword(16) // 使用强密码 114 | if err := database.AddUser(defaultUsername, defaultPassword); err != nil { 115 | log.Printf("Failed to add default admin account: %v", err) 116 | } else { 117 | fmt.Printf("Username: %s%s%s, Password: %s%s%s\n", 118 | colorGreen, defaultUsername, colorReset, 119 | colorYellow, defaultPassword, colorReset) 120 | } 121 | } 122 | 123 | // 初始化HTTP路由 124 | mux := http.NewServeMux() 125 | 126 | // 设置WebSocket处理 127 | mux.HandleFunc("/ws", middleware.IPWhitelistMiddleware(terminal.TerminalHandler)) 128 | 129 | // 设置API路由 130 | mux.HandleFunc("/api/login", middleware.IPWhitelistMiddleware(middleware.CorsMiddleware(api.LoginHandler))) 131 | mux.HandleFunc("/api/status", middleware.IPWhitelistMiddleware(middleware.CorsMiddleware(middleware.TokenAuth(api.StatusHandler)))) 132 | mux.HandleFunc("/api/start", middleware.IPWhitelistMiddleware(middleware.CorsMiddleware(middleware.TokenAuth(api.StartHandler)))) 133 | mux.HandleFunc("/api/stop", middleware.IPWhitelistMiddleware(middleware.CorsMiddleware(middleware.TokenAuth(api.StopHandler)))) 134 | 135 | // 终端会话相关API 136 | mux.HandleFunc("/api/terminals", middleware.IPWhitelistMiddleware(middleware.CorsMiddleware(middleware.TokenAuth(api.ListTerminalSessionsHandler)))) 137 | mux.HandleFunc("/api/terminals/kill", middleware.IPWhitelistMiddleware(middleware.CorsMiddleware(middleware.TokenAuth(api.KillTerminalHandler)))) 138 | 139 | // 命令相关API 140 | mux.HandleFunc("/api/commands", middleware.IPWhitelistMiddleware(middleware.CorsMiddleware(middleware.TokenAuth(api.GetUserCommandsHandler)))) 141 | mux.HandleFunc("/api/commands/add", middleware.IPWhitelistMiddleware(middleware.CorsMiddleware(middleware.TokenAuth(api.AddUserCommandHandler)))) 142 | mux.HandleFunc("/api/commands/update", middleware.IPWhitelistMiddleware(middleware.CorsMiddleware(middleware.TokenAuth(api.UpdateUserCommandHandler)))) 143 | mux.HandleFunc("/api/commands/delete", middleware.IPWhitelistMiddleware(middleware.CorsMiddleware(middleware.TokenAuth(api.DeleteUserCommandHandler)))) 144 | 145 | // 设置静态文件服务 146 | // 初始化嵌入式前端资源 147 | log.Println("Using embedded frontend resources") 148 | webFS, err := fs.Sub(webContent, "web/out") 149 | if err != nil { 150 | log.Fatalf("Failed to access embedded frontend resources: %v", err) 151 | } 152 | fileServer := http.FileServer(http.FS(webFS)) 153 | 154 | // 设置前端路由处理 155 | mux.HandleFunc("/", middleware.IPWhitelistMiddleware(func(w http.ResponseWriter, r *http.Request) { 156 | // 检查请求路径是否是API路径或WS路径 157 | if strings.HasPrefix(r.URL.Path, "/api/") || strings.HasPrefix(r.URL.Path, "/ws") { 158 | http.NotFound(w, r) 159 | return 160 | } 161 | 162 | // 检查路径是否是静态资源(CSS, JS等) 163 | if isStaticResource(r.URL.Path) { 164 | fileServer.ServeHTTP(w, r) 165 | return 166 | } 167 | 168 | // 对于所有其他路径,包括根路径和客户端路由路径,返回index.html 169 | content, err := webContent.ReadFile("web/out/index.html") 170 | if err != nil { 171 | log.Printf("Failed to read embedded index.html: %v\n", err) 172 | http.Error(w, "Frontend file not found", http.StatusInternalServerError) 173 | return 174 | } 175 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 176 | w.Write(content) 177 | })) 178 | 179 | // 创建HTTP服务器 180 | server := &http.Server{ 181 | Addr: ":" + config.GetServerPort(), 182 | Handler: mux, 183 | } 184 | 185 | // 启动终端会话清理定时器 186 | terminal.StartSessionCleaner() 187 | 188 | // 启动服务器 189 | log.Printf("Server started, listening on port: %s\n", config.GetServerPort()) 190 | 191 | // 在单独的goroutine中启动服务器,这样它就不会阻塞优雅退出的处理 192 | go func() { 193 | if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 194 | log.Fatalf("Server listening failed: %v", err) 195 | } 196 | }() 197 | 198 | // 设置优雅退出 199 | gracefulShutdown() 200 | } 201 | 202 | // 判断是否是静态资源 203 | func isStaticResource(path string) bool { 204 | // 常见的静态文件后缀 205 | staticExtensions := []string{ 206 | ".js", ".css", ".html", ".png", ".jpg", ".jpeg", ".gif", 207 | ".svg", ".ico", ".woff", ".woff2", ".ttf", ".eot", ".json", 208 | ".map", ".txt", ".pdf", ".webp", 209 | } 210 | 211 | // 特定的静态目录前缀 212 | staticPrefixes := []string{ 213 | "/_next/", "/images/", "/assets/", "/static/", "/favicon.ico", 214 | } 215 | 216 | // 检查前缀 217 | for _, prefix := range staticPrefixes { 218 | if strings.HasPrefix(path, prefix) { 219 | return true 220 | } 221 | } 222 | 223 | // 检查后缀 224 | for _, ext := range staticExtensions { 225 | if strings.HasSuffix(path, ext) { 226 | return true 227 | } 228 | } 229 | 230 | return false 231 | } 232 | 233 | // 设置优雅关闭 234 | func gracefulShutdown() { 235 | // 监听 Ctrl+C 和 kill 命令 236 | stop := make(chan os.Signal, 1) 237 | signal.Notify(stop, os.Interrupt, syscall.SIGTERM) 238 | 239 | // 阻塞直到收到终止信号 240 | <-stop 241 | 242 | log.Println("Shutting down server...") 243 | 244 | // 这里可以添加任何优雅关闭逻辑,如关闭数据库连接等 245 | database.CloseDatabase() 246 | 247 | log.Println("Server has been safely shut down, goodbye!") 248 | } 249 | -------------------------------------------------------------------------------- /src/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strings" 7 | 8 | "ghosteye/auth" 9 | "ghosteye/database" 10 | "ghosteye/models" 11 | "ghosteye/utils" 12 | ) 13 | 14 | // 上下文键类型 15 | type contextKey string 16 | 17 | // 用户名上下文键 18 | const usernameKey contextKey = "username" 19 | 20 | // ContextWithUsername 将用户名添加到上下文 21 | func ContextWithUsername(ctx context.Context, username string) context.Context { 22 | return context.WithValue(ctx, usernameKey, username) 23 | } 24 | 25 | // UsernameFromContext 从上下文获取用户名 26 | func UsernameFromContext(ctx context.Context) string { 27 | if username, ok := ctx.Value(usernameKey).(string); ok { 28 | return username 29 | } 30 | return "" 31 | } 32 | 33 | // CorsMiddleware 添加CORS支持 34 | func CorsMiddleware(next http.HandlerFunc) http.HandlerFunc { 35 | return func(w http.ResponseWriter, r *http.Request) { 36 | // 设置CORS头 37 | w.Header().Set("Access-Control-Allow-Origin", "*") 38 | w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") 39 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") 40 | 41 | // 处理预检请求 42 | if r.Method == "OPTIONS" { 43 | w.WriteHeader(http.StatusOK) 44 | return 45 | } 46 | 47 | // 调用下一个处理函数 48 | next(w, r) 49 | } 50 | } 51 | 52 | // IPWhitelistMiddleware 检查请求IP是否在白名单中 53 | func IPWhitelistMiddleware(next http.HandlerFunc) http.HandlerFunc { 54 | return func(w http.ResponseWriter, r *http.Request) { 55 | // 获取客户端IP 56 | ip := utils.GetClientIP(r) 57 | 58 | // 检查IP是否在白名单中 59 | if !database.IsIPInWhitelist(ip) { 60 | // 使用http.Hijacker劫持连接并关闭,模拟端口未开放的行为 61 | hj, ok := w.(http.Hijacker) 62 | if ok { 63 | // 获取底层连接并关闭 64 | conn, _, err := hj.Hijack() 65 | if err == nil { 66 | conn.Close() // 直接关闭连接,不发送任何响应 67 | return 68 | } 69 | } 70 | w.WriteHeader(http.StatusGatewayTimeout) 71 | return 72 | } 73 | 74 | // 调用下一个处理函数 75 | next(w, r) 76 | } 77 | } 78 | 79 | // TokenAuth 通过会话令牌进行认证 80 | func TokenAuth(next http.HandlerFunc) http.HandlerFunc { 81 | return func(w http.ResponseWriter, r *http.Request) { 82 | // 设置CORS头 83 | w.Header().Set("Access-Control-Allow-Origin", "*") 84 | w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") 85 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") 86 | 87 | // 处理预检请求 88 | if r.Method == "OPTIONS" { 89 | w.WriteHeader(http.StatusOK) 90 | return 91 | } 92 | 93 | // 获取Authorization头 94 | authHeader := r.Header.Get("Authorization") 95 | if authHeader == "" { 96 | w.WriteHeader(http.StatusUnauthorized) 97 | w.Write([]byte("Authentication token not provided")) 98 | return 99 | } 100 | 101 | // 解析Authorization头 102 | authParts := strings.SplitN(authHeader, " ", 2) 103 | if len(authParts) != 2 || authParts[0] != "Bearer" { 104 | w.WriteHeader(http.StatusUnauthorized) 105 | w.Write([]byte("Invalid authentication format, should be Bearer token")) 106 | return 107 | } 108 | 109 | // 获取令牌 110 | token := authParts[1] 111 | 112 | // 验证令牌 113 | username, valid := auth.ValidateSessionToken(token) 114 | if !valid { 115 | w.WriteHeader(http.StatusUnauthorized) 116 | w.Write([]byte("Invalid or expired token")) 117 | return 118 | } 119 | 120 | // 获取或创建用户状态 121 | auth.GetUserState(username) 122 | 123 | // 将用户名存储在请求上下文中 124 | r = SetUsernameInContext(r, username) 125 | 126 | // 调用下一个处理函数 127 | next(w, r) 128 | } 129 | } 130 | 131 | // SetUsernameInContext 在请求上下文中设置用户名 132 | func SetUsernameInContext(r *http.Request, username string) *http.Request { 133 | ctx := r.Context() 134 | ctx = ContextWithUsername(ctx, username) 135 | return r.WithContext(ctx) 136 | } 137 | 138 | // GetUsernameFromContext 从请求上下文中获取用户名 139 | func GetUsernameFromContext(r *http.Request) string { 140 | return UsernameFromContext(r.Context()) 141 | } 142 | 143 | // GetUserState 从请求中获取用户状态 144 | func GetUserState(r *http.Request) *models.UserState { 145 | username := GetUsernameFromContext(r) 146 | if username == "" { 147 | return nil 148 | } 149 | 150 | return auth.GetUserState(username) 151 | } 152 | -------------------------------------------------------------------------------- /src/models/terminal.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os" 7 | "os/exec" 8 | "sync" 9 | "time" 10 | 11 | "github.com/gorilla/websocket" 12 | ) 13 | 14 | // Message WebSocket消息结构 15 | type Message struct { 16 | Type string `json:"type"` 17 | TerminalID string `json:"terminalId"` // 添加终端ID字段 18 | Data interface{} `json:"data"` 19 | } 20 | 21 | // AuthResponse 认证响应结构 22 | type AuthResponse struct { 23 | Type string `json:"type"` 24 | TerminalID string `json:"terminalId"` // 添加终端ID字段 25 | Success bool `json:"success"` 26 | Error string `json:"error,omitempty"` 27 | Token string `json:"token,omitempty"` 28 | } 29 | 30 | // OutputBuffer 输出缓冲区 31 | type OutputBuffer struct { 32 | Data []byte 33 | Max int // 最大存储字节数 34 | sync.Mutex 35 | } 36 | 37 | // TerminalSession 终端会话 38 | type TerminalSession struct { 39 | ID string // 会话ID 40 | Cmd *exec.Cmd // 命令进程 41 | Ptmx *os.File // PTY主设备 42 | Done chan struct{} // 完成信号 43 | Stdin io.WriteCloser // 标准输入(用于非PTY模式) 44 | IsStandardPipe bool // 是否使用标准管道(非PTY模式) 45 | LastActive time.Time // 上次活跃时间 46 | Mutex sync.Mutex // 锁,确保线程安全 47 | 48 | // 新增字段 49 | Clients map[string]*websocket.Conn // 连接到该会话的客户端 50 | ClientsMutex sync.Mutex // 客户端列表的互斥锁 51 | Buffer OutputBuffer // 输出缓冲区,用于新客户端连接时回放 52 | Active bool // 会话是否活跃 53 | Created time.Time // 会话创建时间 54 | CancelFunc context.CancelFunc // 用于取消goroutine的函数 55 | } 56 | 57 | // 全局终端会话管理 58 | var ( 59 | // 用户名 -> 终端ID -> 会话 60 | TerminalSessions = make(map[string]map[string]*TerminalSession) 61 | TerminalSessionsMux sync.Mutex 62 | ) 63 | 64 | // Append 添加数据到缓冲区 65 | func (b *OutputBuffer) Append(data []byte) { 66 | b.Lock() 67 | defer b.Unlock() 68 | 69 | // 计算新数据后的总大小 70 | newSize := len(b.Data) + len(data) 71 | 72 | // 如果超过最大大小,截断旧数据 73 | if newSize > b.Max { 74 | // 保留最后的数据 75 | excess := newSize - b.Max 76 | if excess < len(b.Data) { 77 | b.Data = b.Data[excess:] 78 | } else { 79 | b.Data = []byte{} 80 | } 81 | } 82 | 83 | // 添加新数据 84 | b.Data = append(b.Data, data...) 85 | } 86 | -------------------------------------------------------------------------------- /src/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // UserState 存储每个用户的状态 8 | type UserState struct { 9 | IsRunning bool // 是否有命令在运行 10 | Command string // 当前运行的命令 11 | Mutex sync.Mutex 12 | } 13 | 14 | // 全局的用户状态管理 15 | var ( 16 | UserStates = make(map[string]*UserState) 17 | UserStatesMux sync.Mutex 18 | // 会话管理 19 | SessionTokens = make(map[string]string) // token -> username 20 | SessionTokensMux sync.Mutex 21 | ) 22 | 23 | // Response 是一个简单的结构体,用于响应客户请求 24 | // Code: 0 表示成功,非0 表示错误 25 | // Message 用于描述错误或成功信息 26 | // Data 是额外响应数据 27 | type Response struct { 28 | Code int `json:"code"` 29 | Message string `json:"message"` 30 | Data interface{} `json:"data,omitempty"` 31 | } 32 | 33 | // AuthRequest 认证请求结构 34 | type AuthRequest struct { 35 | Username string `json:"username"` 36 | Password string `json:"password"` 37 | } 38 | 39 | // UserCommand 用户自定义命令 40 | type UserCommand struct { 41 | ID int `json:"id"` 42 | Username string `json:"username"` 43 | Name string `json:"name"` 44 | Command string `json:"command"` 45 | Description string `json:"description"` 46 | } 47 | 48 | // TerminalSession 数据库中的终端会话记录 49 | type TerminalSessionRecord struct { 50 | ID int `json:"id"` 51 | TerminalID string `json:"terminal_id"` 52 | Username string `json:"username"` 53 | Buffer []byte `json:"buffer"` 54 | CreatedAt string `json:"created_at"` 55 | LastActive string `json:"last_active"` 56 | Active bool `json:"active"` 57 | } -------------------------------------------------------------------------------- /src/terminal/handler.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "math/rand" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/gorilla/websocket" 12 | 13 | "ghosteye/auth" 14 | 15 | ) 16 | 17 | // WebSocket升级器 18 | var upgrader = websocket.Upgrader{ 19 | ReadBufferSize: 4096, // 增加缓冲区大小 20 | WriteBufferSize: 4096, // 增加缓冲区大小 21 | CheckOrigin: func(r *http.Request) bool { 22 | return true // 允许所有来源 23 | }, 24 | } 25 | 26 | // TerminalHandler 处理WebSocket终端连接 27 | func TerminalHandler(w http.ResponseWriter, r *http.Request) { 28 | // 设置CORS头 29 | w.Header().Set("Access-Control-Allow-Origin", "*") 30 | w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") 31 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") 32 | 33 | // 处理预检请求 34 | if r.Method == "OPTIONS" { 35 | w.WriteHeader(http.StatusOK) 36 | return 37 | } 38 | 39 | // 获取终端ID(如果存在) 40 | terminalID := r.URL.Query().Get("terminalId") 41 | if terminalID == "" { 42 | // 如果未提供终端ID,生成一个新的,格式与前端一致: term_timestamp_random 43 | timestamp := time.Now().UnixNano() 44 | random := rand.Intn(10000) 45 | terminalID = fmt.Sprintf("%d_%d", timestamp, random) 46 | } 47 | 48 | // 检查URL参数中是否有token 49 | tokens, ok := r.URL.Query()["token"] 50 | var username string 51 | var isAuthenticated bool 52 | 53 | // 首先尝试通过URL参数中的token认证 54 | if ok && len(tokens) > 0 { 55 | token := tokens[0] 56 | 57 | // 验证token并获取用户名 58 | var valid bool 59 | username, valid = auth.ValidateToken(token) 60 | if valid { 61 | isAuthenticated = true 62 | log.Printf("User %s authenticated successfully via URL token, %s", username, r.RemoteAddr) 63 | } 64 | } 65 | 66 | // 如果URL参数认证失败,返回错误 67 | if !isAuthenticated { 68 | http.Error(w, "Unauthorized", http.StatusUnauthorized) 69 | return 70 | } 71 | 72 | // 配置WebSocket升级器 73 | upgrader := websocket.Upgrader{ 74 | ReadBufferSize: 4096, 75 | WriteBufferSize: 4096, 76 | CheckOrigin: func(r *http.Request) bool { 77 | return true // 允许所有来源 78 | }, 79 | } 80 | 81 | // 升级HTTP连接为WebSocket 82 | conn, err := upgrader.Upgrade(w, r, nil) 83 | if err != nil { 84 | log.Printf("WebSocket upgrade error: %v, %s", err, r.RemoteAddr) 85 | return 86 | } 87 | 88 | // 发送简短的欢迎消息 89 | welcomeMsg := struct { 90 | Type string `json:"type"` 91 | TerminalID string `json:"terminalId"` 92 | Data string `json:"data"` 93 | }{ 94 | Type: "welcome", 95 | TerminalID: terminalID, 96 | Data: "Terminal connected\r\n", 97 | } 98 | welcomeBytes, _ := json.Marshal(welcomeMsg) 99 | conn.WriteMessage(websocket.TextMessage, welcomeBytes) 100 | 101 | // 发送认证成功响应 102 | successResp := struct { 103 | Type string `json:"type"` 104 | TerminalID string `json:"terminalId"` 105 | Success bool `json:"success"` 106 | Token string `json:"token,omitempty"` 107 | }{ 108 | Type: "auth", 109 | TerminalID: terminalID, 110 | Success: true, 111 | } 112 | 113 | respBytes, _ := json.Marshal(successResp) 114 | conn.WriteMessage(websocket.TextMessage, respBytes) 115 | 116 | // 处理终端 117 | HandleUnixTerminal(conn, r.RemoteAddr, terminalID, username) 118 | } 119 | -------------------------------------------------------------------------------- /src/terminal/session.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "syscall" 7 | "time" 8 | 9 | "ghosteye/database" 10 | "ghosteye/models" 11 | ) 12 | 13 | // 获取用户的终端会话 14 | func GetUserSessions(username string) map[string]*models.TerminalSession { 15 | models.TerminalSessionsMux.Lock() 16 | defer models.TerminalSessionsMux.Unlock() 17 | 18 | sessions, exists := models.TerminalSessions[username] 19 | if !exists { 20 | sessions = make(map[string]*models.TerminalSession) 21 | models.TerminalSessions[username] = sessions 22 | } 23 | 24 | return sessions 25 | } 26 | 27 | // 获取特定终端会话 28 | func GetTerminalSession(username, terminalID string) *models.TerminalSession { 29 | models.TerminalSessionsMux.Lock() 30 | defer models.TerminalSessionsMux.Unlock() 31 | 32 | sessions, exists := models.TerminalSessions[username] 33 | if !exists { 34 | return nil 35 | } 36 | 37 | return sessions[terminalID] 38 | } 39 | 40 | // 保存终端会话 41 | func SaveTerminalSession(username, terminalID string, session *models.TerminalSession) { 42 | models.TerminalSessionsMux.Lock() 43 | defer models.TerminalSessionsMux.Unlock() 44 | 45 | sessions, exists := models.TerminalSessions[username] 46 | if !exists { 47 | sessions = make(map[string]*models.TerminalSession) 48 | models.TerminalSessions[username] = sessions 49 | } 50 | 51 | sessions[terminalID] = session 52 | } 53 | 54 | // 移除终端会话 55 | func RemoveTerminalSession(username, terminalID string) { 56 | models.TerminalSessionsMux.Lock() 57 | defer models.TerminalSessionsMux.Unlock() 58 | 59 | sessions, exists := models.TerminalSessions[username] 60 | if !exists { 61 | return 62 | } 63 | 64 | // 关闭会话 65 | session, exists := sessions[terminalID] 66 | if exists { 67 | // 关闭所有客户端连接 68 | session.ClientsMutex.Lock() 69 | for _, conn := range session.Clients { 70 | conn.Close() 71 | } 72 | session.ClientsMutex.Unlock() 73 | 74 | // 关闭PTY 75 | if session.Ptmx != nil { 76 | session.Ptmx.Close() 77 | } 78 | 79 | // 关闭标准输入 80 | if session.Stdin != nil { 81 | session.Stdin.Close() 82 | } 83 | 84 | // 发送完成信号 85 | close(session.Done) 86 | } 87 | 88 | // 从映射中删除 89 | delete(sessions, terminalID) 90 | } 91 | 92 | // 保存终端会话到数据库 93 | func SaveSessionToDatabase(username string, terminalID string, session *models.TerminalSession) { 94 | // 获取缓冲区数据 95 | session.Mutex.Lock() 96 | buffer := session.Buffer.Data 97 | session.Mutex.Unlock() 98 | 99 | // 保存到数据库 100 | err := database.SaveTerminalSessionToDB(username, terminalID, buffer) 101 | if err != nil { 102 | log.Printf("Failed to save terminal session to database: %v", err) 103 | } 104 | } 105 | 106 | // 将终端会话标记为非活跃 107 | func MarkSessionInactive(username string, terminalID string) { 108 | // 更新数据库中的活跃状态 109 | err := database.SetTerminalSessionActive(username, terminalID, false) 110 | if err != nil { 111 | log.Printf("Failed to mark terminal session as inactive: %v", err) 112 | } 113 | } 114 | 115 | // 启动会话清理器 116 | func StartSessionCleaner() { 117 | go func() { 118 | ticker := time.NewTicker(10 * time.Minute) 119 | defer ticker.Stop() 120 | 121 | for range ticker.C { 122 | CleanInactiveSessions() 123 | } 124 | }() 125 | } 126 | 127 | // 清理非活跃会话 128 | func CleanInactiveSessions() { 129 | // 清理内存中的非活跃会话 130 | models.TerminalSessionsMux.Lock() 131 | now := time.Now() 132 | 133 | for username, sessions := range models.TerminalSessions { 134 | for terminalID, session := range sessions { 135 | // 检查会话是否超过30分钟未活跃 136 | if now.Sub(session.LastActive) > 30*time.Minute { 137 | // 保存会话到数据库 138 | SaveSessionToDatabase(username, terminalID, session) 139 | 140 | // 标记为非活跃 141 | MarkSessionInactive(username, terminalID) 142 | 143 | // 关闭会话 144 | // 关闭所有客户端连接 145 | session.ClientsMutex.Lock() 146 | for _, conn := range session.Clients { 147 | conn.Close() 148 | } 149 | session.ClientsMutex.Unlock() 150 | 151 | // 关闭PTY 152 | if session.Ptmx != nil { 153 | session.Ptmx.Close() 154 | } 155 | 156 | // 关闭标准输入 157 | if session.Stdin != nil { 158 | session.Stdin.Close() 159 | } 160 | 161 | // 发送完成信号 162 | close(session.Done) 163 | 164 | // 从映射中删除 165 | delete(sessions, terminalID) 166 | 167 | log.Printf("Cleaned inactive terminal session %s for user %s", terminalID, username) 168 | } 169 | } 170 | 171 | // 如果用户没有会话,删除用户映射 172 | if len(sessions) == 0 { 173 | delete(models.TerminalSessions, username) 174 | } 175 | } 176 | 177 | models.TerminalSessionsMux.Unlock() 178 | 179 | // 清理数据库中的旧会话(超过7天的非活跃会话) 180 | err := database.CleanupOldTerminalSessions(7) 181 | if err != nil { 182 | log.Printf("Failed to clean old sessions from database: %v", err) 183 | } 184 | } 185 | 186 | // 列出终端会话 187 | func ListTerminals(username string) []map[string]interface{} { 188 | // 从数据库获取会话列表 189 | sessions, err := database.GetUserTerminalSessions(username) 190 | if err != nil { 191 | log.Printf("Failed to get user terminal sessions: %v", err) 192 | return []map[string]interface{}{} 193 | } 194 | 195 | // 添加内存中的活跃会话 196 | models.TerminalSessionsMux.Lock() 197 | defer models.TerminalSessionsMux.Unlock() 198 | 199 | userSessions, exists := models.TerminalSessions[username] 200 | if exists { 201 | for terminalID, session := range userSessions { 202 | // 检查是否已经在列表中 203 | found := false 204 | for _, s := range sessions { 205 | if s["terminal_id"] == terminalID { 206 | found = true 207 | break 208 | } 209 | } 210 | 211 | if !found { 212 | // 添加到列表 213 | sessions = append(sessions, map[string]interface{}{ 214 | "terminal_id": terminalID, 215 | "created_at": session.Created.Format("2006-01-02 15:04:05"), 216 | "last_active": session.LastActive.Format("2006-01-02 15:04:05"), 217 | "active": true, 218 | "age": time.Since(session.LastActive).String(), 219 | "duration": session.LastActive.Sub(session.Created).String(), 220 | }) 221 | } 222 | } 223 | } 224 | 225 | return sessions 226 | } 227 | 228 | // KillTerminalSession 强制终止终端会话 229 | func KillTerminalSession(username, terminalID string) error { 230 | models.TerminalSessionsMux.Lock() 231 | defer models.TerminalSessionsMux.Unlock() 232 | 233 | sessions, exists := models.TerminalSessions[username] 234 | if !exists { 235 | return fmt.Errorf("No terminal sessions exist for user %s", username) 236 | } 237 | 238 | session, exists := sessions[terminalID] 239 | if !exists { 240 | return fmt.Errorf("Terminal session %s does not exist", terminalID) 241 | } 242 | 243 | // 终止进程 244 | if session.Cmd != nil && session.Cmd.Process != nil { 245 | // 尝试先优雅地终止进程 246 | err := session.Cmd.Process.Signal(syscall.SIGTERM) 247 | if err != nil { 248 | log.Printf("Failed to send SIGTERM: %v, attempting SIGKILL", err) 249 | // If SIGTERM fails, use SIGKILL to force termination 250 | err = session.Cmd.Process.Kill() 251 | if err != nil { 252 | return fmt.Errorf("Failed to terminate process: %v", err) 253 | } 254 | } 255 | 256 | // 等待进程退出 257 | go func() { 258 | session.Cmd.Wait() 259 | log.Printf("Process for terminal session %s has been terminated", terminalID) 260 | }() 261 | } 262 | 263 | // 在这里完全从数据库中删除终端会话,而不是保存它 264 | err := database.DeleteTerminalSessionFromDB(username, terminalID) 265 | if err != nil { 266 | log.Printf("Failed to delete terminal session from database: %v", err) 267 | } 268 | 269 | // 关闭所有客户端连接 270 | session.ClientsMutex.Lock() 271 | for clientIP, conn := range session.Clients { 272 | log.Printf("Closing connection for client %s", clientIP) 273 | conn.Close() 274 | } 275 | session.ClientsMutex.Unlock() 276 | 277 | // 关闭PTY 278 | if session.Ptmx != nil { 279 | session.Ptmx.Close() 280 | } 281 | 282 | // 关闭标准输入 283 | if session.Stdin != nil { 284 | session.Stdin.Close() 285 | } 286 | 287 | // 发送完成信号 288 | close(session.Done) 289 | 290 | // 从映射中删除 291 | delete(sessions, terminalID) 292 | 293 | return nil 294 | } 295 | -------------------------------------------------------------------------------- /src/terminal/unix.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "sync" 11 | "syscall" 12 | "time" 13 | "unsafe" 14 | "encoding/json" 15 | "encoding/base64" 16 | 17 | "github.com/creack/pty" 18 | "github.com/gorilla/websocket" 19 | 20 | "ghosteye/database" 21 | "ghosteye/models" 22 | ) 23 | 24 | // 处理Unix终端会话 25 | func HandleUnixTerminal(conn *websocket.Conn, clientIP string, terminalID string, username string) { 26 | // 创建新会话或获取现有会话 27 | session := GetTerminalSession(username, terminalID) 28 | 29 | // 如果会话存在且有活跃的PTY,直接连接到现有会话 30 | if session != nil && (session.Ptmx != nil || session.Cmd != nil) { 31 | log.Printf("Client %s connected to existing session %s", clientIP, terminalID) 32 | 33 | // 将客户端添加到会话中 34 | session.ClientsMutex.Lock() 35 | session.Clients[clientIP] = conn 36 | session.ClientsMutex.Unlock() 37 | 38 | // 更新会话活跃时间 39 | session.LastActive = time.Now() 40 | 41 | // 向客户端发送缓冲数据 42 | if len(session.Buffer.Data) > 0 { 43 | conn.WriteMessage(websocket.BinaryMessage, session.Buffer.Data) 44 | } 45 | 46 | // 处理WebSocket连接 47 | handleWebSocketConnection(conn, clientIP, session, username, terminalID) 48 | return 49 | } 50 | 51 | // 会话不存在或已关闭,尝试从数据库恢复或创建新会话 52 | if session == nil { 53 | // 尝试从数据库加载会话 54 | buffer, _, err := database.LoadTerminalSessionFromDB(username, terminalID) 55 | if err == nil && buffer != nil { 56 | // Recover session from database 57 | log.Printf("Recovering terminal session %s for user %s from database", terminalID, username) 58 | session = &models.TerminalSession{ 59 | Done: make(chan struct{}), 60 | LastActive: time.Now(), 61 | Clients: make(map[string]*websocket.Conn), 62 | Buffer: models.OutputBuffer{ 63 | Data: buffer, 64 | Max: 100 * 1024, // 最大100KB 65 | }, 66 | Active: true, 67 | Created: time.Now(), 68 | } 69 | SaveTerminalSession(username, terminalID, session) 70 | } else { 71 | // 创建新会话 72 | session = &models.TerminalSession{ 73 | Done: make(chan struct{}), 74 | LastActive: time.Now(), 75 | Clients: make(map[string]*websocket.Conn), 76 | Buffer: models.OutputBuffer{ 77 | Data: []byte{}, 78 | Max: 100 * 1024, // 最大100KB 79 | }, 80 | Active: true, 81 | Created: time.Now(), 82 | } 83 | SaveTerminalSession(username, terminalID, session) 84 | } 85 | } 86 | 87 | // 将客户端添加到会话中 88 | session.ClientsMutex.Lock() 89 | session.Clients[clientIP] = conn 90 | session.ClientsMutex.Unlock() 91 | 92 | log.Printf("Client %s connected to session %s, current client count: %d", clientIP, terminalID, len(session.Clients)) 93 | 94 | // 更新会话活跃时间 95 | session.LastActive = time.Now() 96 | 97 | // 向客户端发送缓冲数据 98 | if len(session.Buffer.Data) > 0 { 99 | conn.WriteMessage(websocket.BinaryMessage, session.Buffer.Data) 100 | conn.WriteMessage(websocket.BinaryMessage, []byte("\r\n--- History ends, new session begins ---\r\n")) 101 | } 102 | 103 | // 创建命令 104 | cmd := exec.Command("/bin/bash") 105 | 106 | // 创建伪终端 107 | ptmx, err := pty.Start(cmd) 108 | if err != nil { 109 | log.Printf("Failed to start PTY: %v, %s", err, clientIP) 110 | conn.WriteMessage(websocket.BinaryMessage, []byte(fmt.Sprintf("Failed to start terminal: %v\r\n", err))) 111 | return 112 | } 113 | 114 | // 将PTY和命令保存到会话中 115 | session.Ptmx = ptmx 116 | session.Cmd = cmd 117 | 118 | // 设置终端大小 119 | pty.Setsize(ptmx, &pty.Winsize{ 120 | Rows: 24, 121 | Cols: 80, 122 | }) 123 | 124 | // 创建用于取消goroutine的上下文 125 | ctx, cancel := context.WithCancel(context.Background()) 126 | session.CancelFunc = cancel 127 | 128 | // 创建WaitGroup等待所有goroutine 129 | var wg sync.WaitGroup 130 | 131 | // 从PTY读取并写入WebSocket 132 | wg.Add(1) 133 | go func() { 134 | defer wg.Done() 135 | buf := make([]byte, 4096) 136 | for { 137 | select { 138 | case <-ctx.Done(): 139 | return 140 | default: 141 | // 设置读取超时以避免阻塞 142 | ptmx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) 143 | n, err := ptmx.Read(buf) 144 | if err != nil { 145 | if err != io.EOF && !os.IsTimeout(err) { 146 | log.Printf("Failed to read from PTY: %v, %s", err, clientIP) 147 | } 148 | // 超时错误是正常的,继续读取 149 | if os.IsTimeout(err) { 150 | continue 151 | } 152 | // 其他错误,退出循环 153 | return 154 | } 155 | 156 | if n > 0 { 157 | // 将输出添加到缓冲区 158 | session.Buffer.Append(buf[:n]) 159 | 160 | // 构造带终端ID的消息 161 | message := struct { 162 | Type string `json:"type"` 163 | TerminalID string `json:"terminalId"` 164 | Data string `json:"data"` 165 | }{ 166 | Type: "output", 167 | TerminalID: terminalID, 168 | // 使用Base64编码二进制数据 169 | Data: base64.StdEncoding.EncodeToString(buf[:n]), 170 | } 171 | 172 | // 序列化消息 173 | messageBytes, err := json.Marshal(message) 174 | if err != nil { 175 | log.Printf("Failed to serialize terminal output message: %v", err) 176 | continue 177 | } 178 | 179 | // 向所有客户端发送输出 180 | session.ClientsMutex.Lock() 181 | for clientAddr, clientConn := range session.Clients { 182 | err := clientConn.WriteMessage(websocket.TextMessage, messageBytes) 183 | if err != nil { 184 | log.Printf("Failed to send message to client %s: %v", clientAddr, err) 185 | // Don't remove client here to avoid concurrent map modification 186 | } 187 | } 188 | session.ClientsMutex.Unlock() 189 | } 190 | } 191 | } 192 | }() 193 | 194 | // 处理WebSocket连接 195 | handleWebSocketConnection(conn, clientIP, session, username, terminalID) 196 | 197 | } 198 | 199 | // 处理WebSocket连接 200 | func handleWebSocketConnection(conn *websocket.Conn, clientIP string, session *models.TerminalSession, username string, terminalID string) { 201 | // 从WebSocket读取并写入PTY 202 | for { 203 | // 设置较长的读取超时以避免频繁超时 204 | conn.SetReadDeadline(time.Now().Add(5 * time.Second)) 205 | messageType, p, err := conn.ReadMessage() 206 | if err != nil { 207 | // 重置读取超时 208 | conn.SetReadDeadline(time.Time{}) 209 | 210 | if websocket.IsCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) { 211 | log.Printf("Client %s closed connection normally", clientIP) 212 | } else { 213 | log.Printf("Failed to read WebSocket message: %v, %s", err, clientIP) 214 | } 215 | 216 | // 从会话中移除客户端 217 | session.ClientsMutex.Lock() 218 | delete(session.Clients, clientIP) 219 | clientCount := len(session.Clients) 220 | session.ClientsMutex.Unlock() 221 | 222 | log.Printf("Client %s disconnected, %d clients remaining", clientIP, clientCount) 223 | 224 | // 如果没有更多客户端但命令仍在运行,保持会话活跃 225 | if clientCount == 0 { 226 | log.Printf("Command still running for session %s, keeping active", terminalID) 227 | // Save session to database 228 | SaveSessionToDatabase(username, terminalID, session) 229 | } 230 | 231 | return 232 | } 233 | 234 | // 重置读取超时 235 | conn.SetReadDeadline(time.Time{}) 236 | 237 | // 处理不同类型的消息 238 | if messageType == websocket.TextMessage { 239 | // 尝试解析为JSON 240 | var message models.Message 241 | if err := json.Unmarshal(p, &message); err == nil { 242 | if message.Type == "resize" { 243 | // 处理终端大小调整 244 | if resizeData, ok := message.Data.(map[string]interface{}); ok { 245 | if cols, ok := resizeData["cols"].(float64); ok { 246 | if rows, ok := resizeData["rows"].(float64); ok { 247 | // 调整终端大小 248 | if session.Ptmx != nil { 249 | pty.Setsize(session.Ptmx, &pty.Winsize{ 250 | Rows: uint16(rows), 251 | Cols: uint16(cols), 252 | }) 253 | } 254 | } 255 | } 256 | } 257 | continue 258 | } else if message.Type == "heartbeat" { 259 | // 处理心跳消息 260 | // 更新会话活跃时间 261 | session.LastActive = time.Now() 262 | 263 | // 发送心跳响应 264 | heartbeatResp := struct { 265 | Type string `json:"type"` 266 | TerminalID string `json:"terminalId"` 267 | Data string `json:"data"` 268 | }{ 269 | Type: "heartbeat", 270 | TerminalID: terminalID, 271 | Data: "pong", 272 | } 273 | 274 | respBytes, _ := json.Marshal(heartbeatResp) 275 | 276 | // 延迟心跳响应以避免过于频繁的通信 277 | time.Sleep(200 * time.Millisecond) 278 | 279 | if err := conn.WriteMessage(websocket.TextMessage, respBytes); err != nil { 280 | // Don't disconnect on heartbeat response failure, just log error 281 | log.Printf("Failed to send heartbeat response: %v, %s", err, clientIP) 282 | } 283 | continue 284 | } else if message.Type == "close" { 285 | // Handle close command 286 | log.Printf("Received close command from client %s", clientIP) 287 | 288 | // 从会话中移除客户端 289 | session.ClientsMutex.Lock() 290 | delete(session.Clients, clientIP) 291 | clientCount := len(session.Clients) 292 | session.ClientsMutex.Unlock() 293 | 294 | // 如果没有更多客户端,关闭会话 295 | if clientCount == 0 { 296 | // 将会话保存到数据库 297 | SaveSessionToDatabase(username, terminalID, session) 298 | 299 | // 如果用户明确请求关闭,终止进程 300 | if session.Cmd != nil && session.Cmd.Process != nil { 301 | log.Printf("Terminating command for session %s", terminalID) 302 | session.Cmd.Process.Signal(syscall.SIGTERM) 303 | time.Sleep(100 * time.Millisecond) 304 | session.Cmd.Process.Kill() 305 | } 306 | 307 | // 标记为非活跃 308 | MarkSessionInactive(username, terminalID) 309 | 310 | // 移除会话 311 | RemoveTerminalSession(username, terminalID) 312 | } 313 | 314 | return 315 | } 316 | } 317 | 318 | // 其他文本消息,写入PTY 319 | if session.Ptmx != nil { 320 | _, err = session.Ptmx.Write(p) 321 | if err != nil { 322 | log.Printf("Failed to write to PTY: %v, %s", err, clientIP) 323 | } 324 | } 325 | } else if messageType == websocket.BinaryMessage { 326 | // 二进制消息,直接写入PTY 327 | if session.Ptmx != nil { 328 | _, err = session.Ptmx.Write(p) 329 | if err != nil { 330 | log.Printf("Failed to write to PTY: %v, %s", err, clientIP) 331 | } 332 | } 333 | } 334 | 335 | // 更新会话活跃时间 336 | session.LastActive = time.Now() 337 | } 338 | } 339 | 340 | // 创建新会话 341 | func createNewSession(conn *websocket.Conn, remoteAddr string, username string, terminalID string) *models.TerminalSession { 342 | // 创建新会话 343 | session := &models.TerminalSession{ 344 | Done: make(chan struct{}), 345 | LastActive: time.Now(), 346 | Clients: make(map[string]*websocket.Conn), 347 | Buffer: models.OutputBuffer{Max: 100 * 1024}, // 100KB缓冲区 348 | Active: true, 349 | Created: time.Now(), 350 | } 351 | 352 | // 添加客户端 353 | session.ClientsMutex.Lock() 354 | session.Clients[remoteAddr] = conn 355 | session.ClientsMutex.Unlock() 356 | 357 | // 保存会话 358 | SaveTerminalSession(username, terminalID, session) 359 | 360 | return session 361 | } 362 | 363 | // 启动终端命令 364 | func startTerminalCommand(session *models.TerminalSession, conn *websocket.Conn, remoteAddr string, username string, terminalID string) { 365 | // 确定shell命令 366 | shell := os.Getenv("SHELL") 367 | if shell == "" { 368 | shell = "/bin/bash" 369 | } 370 | 371 | // 创建命令 372 | cmd := exec.Command(shell, "-l") // 使用登录shell 373 | 374 | // 设置环境变量 375 | cmd.Env = append(os.Environ(), "TERM=xterm-256color") 376 | 377 | var err error 378 | 379 | // 创建PTY 380 | session.Ptmx, err = pty.Start(cmd) 381 | if err != nil { 382 | log.Printf("Failed to start PTY: %v", err) 383 | conn.WriteMessage(websocket.BinaryMessage, []byte("Failed to start terminal: "+err.Error()+"\r\n")) 384 | conn.Close() 385 | return 386 | } 387 | session.IsStandardPipe = false 388 | 389 | // 保存命令 390 | session.Cmd = cmd 391 | 392 | // 启动读取goroutine 393 | go func() { 394 | buf := make([]byte, 32*1024) 395 | for { 396 | n, err := session.Ptmx.Read(buf) 397 | if err != nil { 398 | if err != io.EOF { 399 | log.Printf("Failed to read from PTY: %v", err) 400 | } 401 | break 402 | } 403 | 404 | if n > 0 { 405 | data := buf[:n] 406 | 407 | // 更新最后活跃时间 408 | session.LastActive = time.Now() 409 | 410 | // 添加到缓冲区 411 | session.Mutex.Lock() 412 | session.Buffer.Append(data) 413 | session.Mutex.Unlock() 414 | 415 | // 广播给所有客户端 416 | session.ClientsMutex.Lock() 417 | for _, client := range session.Clients { 418 | client.WriteMessage(websocket.BinaryMessage, data) 419 | } 420 | session.ClientsMutex.Unlock() 421 | } 422 | } 423 | 424 | // PTY closed, close session 425 | log.Printf("PTY closed, terminating session %s", terminalID) 426 | 427 | // 将会话保存到数据库 428 | SaveSessionToDatabase(username, terminalID, session) 429 | 430 | // 标记为非活跃 431 | MarkSessionInactive(username, terminalID) 432 | 433 | // 移除会话 434 | RemoveTerminalSession(username, terminalID) 435 | }() 436 | } 437 | 438 | // 连接到现有会话 439 | func ConnectToExistingSession(session *models.TerminalSession, conn *websocket.Conn, remoteAddr string) { 440 | log.Printf("Client %s connected to existing session", remoteAddr) 441 | 442 | // 添加客户端 443 | session.ClientsMutex.Lock() 444 | session.Clients[remoteAddr] = conn 445 | session.ClientsMutex.Unlock() 446 | 447 | // 发送缓冲数据 448 | session.Mutex.Lock() 449 | if len(session.Buffer.Data) > 0 { 450 | conn.WriteMessage(websocket.BinaryMessage, session.Buffer.Data) 451 | } 452 | session.Mutex.Unlock() 453 | 454 | // 启动WebSocket读取goroutine 455 | go func() { 456 | for { 457 | messageType, p, err := conn.ReadMessage() 458 | if err != nil { 459 | log.Printf("Failed to read WebSocket message: %v, %s", err, remoteAddr) 460 | break 461 | } 462 | 463 | // 更新最后活跃时间 464 | session.LastActive = time.Now() 465 | 466 | if messageType == websocket.TextMessage { 467 | // 处理文本消息(JSON控制消息) 468 | var message models.Message 469 | if err := json.Unmarshal(p, &message); err == nil { 470 | if message.Type == "resize" { 471 | // 处理终端大小调整 472 | if resizeData, ok := message.Data.(map[string]interface{}); ok { 473 | cols, _ := resizeData["cols"].(float64) 474 | rows, _ := resizeData["rows"].(float64) 475 | 476 | // 调整终端大小 477 | if !session.IsStandardPipe && session.Ptmx != nil { 478 | ResizeTerminal(session.Ptmx, int(cols), int(rows)) 479 | } 480 | } 481 | } else if message.Type == "heartbeat" { 482 | // 处理心跳消息 483 | heartbeatResp := models.Message{ 484 | Type: "heartbeat", 485 | Data: "pong", 486 | } 487 | respBytes, _ := json.Marshal(heartbeatResp) 488 | conn.WriteMessage(websocket.TextMessage, respBytes) 489 | } 490 | } 491 | } else if messageType == websocket.BinaryMessage { 492 | // 处理二进制消息(终端输入) 493 | if !session.IsStandardPipe && session.Ptmx != nil { 494 | session.Ptmx.Write(p) 495 | } 496 | } 497 | } 498 | 499 | // WebSocket关闭,移除客户端 500 | session.ClientsMutex.Lock() 501 | delete(session.Clients, remoteAddr) 502 | clientCount := len(session.Clients) 503 | session.ClientsMutex.Unlock() 504 | 505 | log.Printf("Client %s disconnected, %d clients remaining", remoteAddr, clientCount) 506 | }() 507 | } 508 | 509 | // 调整终端大小 510 | func ResizeTerminal(ptmx *os.File, cols, rows int) { 511 | ws := struct { 512 | Rows uint16 513 | Cols uint16 514 | XPixel uint16 515 | YPixel uint16 516 | }{ 517 | Rows: uint16(rows), 518 | Cols: uint16(cols), 519 | XPixel: 0, 520 | YPixel: 0, 521 | } 522 | 523 | // TIOCSWINSZ是设置终端窗口大小的ioctl命令 524 | const TIOCSWINSZ = 0x5414 525 | 526 | _, _, errno := syscall.Syscall( 527 | syscall.SYS_IOCTL, 528 | ptmx.Fd(), 529 | uintptr(TIOCSWINSZ), 530 | uintptr(unsafe.Pointer(&ws)), 531 | ) 532 | 533 | if errno != 0 { 534 | log.Printf("Failed to resize terminal: %v", errno) 535 | } 536 | } 537 | -------------------------------------------------------------------------------- /src/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "encoding/json" 7 | "net/http" 8 | "strings" 9 | 10 | "ghosteye/models" 11 | ) 12 | 13 | // WriteJSON 用于将响应写入HTTP响应中 14 | func WriteJSON(w http.ResponseWriter, resp models.Response) { 15 | w.Header().Set("Content-Type", "application/json") 16 | json.NewEncoder(w).Encode(resp) 17 | } 18 | 19 | // GenerateSessionID 生成随机session id 20 | func GenerateSessionID() string { 21 | b := make([]byte, 16) 22 | if _, err := rand.Read(b); err != nil { 23 | return "" 24 | } 25 | return hex.EncodeToString(b) 26 | } 27 | 28 | // GenerateToken 生成会话token 29 | func GenerateToken() string { 30 | b := make([]byte, 32) 31 | if _, err := rand.Read(b); err != nil { 32 | return "" 33 | } 34 | return hex.EncodeToString(b) 35 | } 36 | 37 | // GetClientIP 获取客户端真实IP地址 38 | func GetClientIP(r *http.Request) string { 39 | // 直接从 RemoteAddr 获取,这是 TCP 连接的实际来源 IP 40 | // RemoteAddr 格式为"IP:端口",需要提取 IP 部分 41 | remoteAddr := r.RemoteAddr 42 | if remoteAddr == "" { 43 | return "" 44 | } 45 | 46 | // 提取 IP 部分 47 | if strings.Contains(remoteAddr, ":") { 48 | return strings.Split(remoteAddr, ":")[0] 49 | } 50 | 51 | return remoteAddr 52 | } -------------------------------------------------------------------------------- /src/web/out/404.html: -------------------------------------------------------------------------------- 1 | 404: This page could not be found.GhostEye

404

This page could not be found.

    -------------------------------------------------------------------------------- /src/web/out/_next/static/TFz4HhDxhTU5FA-Rc0bx2/_buildManifest.js: -------------------------------------------------------------------------------- 1 | self.__BUILD_MANIFEST={__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/_error":["static/chunks/pages/_error-7ba65e1336b92748.js"],sortedPages:["/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB(); -------------------------------------------------------------------------------- /src/web/out/_next/static/TFz4HhDxhTU5FA-Rc0bx2/_ssgManifest.js: -------------------------------------------------------------------------------- 1 | self.__SSG_MANIFEST=new Set([]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB() -------------------------------------------------------------------------------- /src/web/out/_next/static/chunks/223.f420fe603d72c60d.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[223],{223:function(t,r,n){n.r(r),n.d(r,{Terminal:function(){return b}});var o=n(7437),a=n(29),l=n.n(a),i=n(2265),c=n(9486),s=n(5896),d=n(1447),u=n(9604),m=n(6070);n(7489);let f=()=>"",w="terminal_auth_token",v={},h="utf-8";function b(t){let{id:r,config:n,onClose:a}=t,b=(0,i.useRef)(null),[p,y]=(0,i.useState)(!1),[x,g]=(0,i.useState)(null),{ip:k,port:S}=n,[E,N]=(0,i.useState)(!1),[T,j]=(0,i.useState)(!1),C=(0,i.useRef)(""),[O,W]=(0,i.useState)("calc(100% - 40px)"),A=(0,i.useRef)(null),L=(0,i.useRef)(0),z=(0,i.useRef)(null),D=(0,i.useRef)(!1),I=(0,i.useRef)(null),[P,F]=(0,i.useState)(!1);return(0,i.useEffect)(()=>{let t=localStorage.getItem(w);t&&(g(t),y(!0))},[]),(0,i.useEffect)(()=>{let t=t=>{var n,o,a,l;let{terminalId:i,command:c}=t.detail;if(i===r&&(null===(o=v[r])||void 0===o?void 0:null===(n=o.websocket)||void 0===n?void 0:n.readyState)===WebSocket.OPEN){let t=c.endsWith("\n")?c:c+"\n";null===(a=v[r].websocket)||void 0===a||a.send(t),null===(l=v[r].terminal)||void 0===l||l.write("\r\n$ ".concat(c,"\r\n"))}};window.addEventListener("terminal-command",t);let n=t=>{let{terminalId:n}=t.detail;if(n===r){var o,a,l,i;if((null===(a=v[r])||void 0===a?void 0:null===(o=a.websocket)||void 0===o?void 0:o.readyState)===WebSocket.OPEN)try{let t=JSON.stringify({type:"close",data:{terminalId:r}});null===(l=v[r].websocket)||void 0===l||l.send(t),null===(i=v[r].terminal)||void 0===i||i.write("\r\nTerminal closing...\r\n"),setTimeout(()=>{var t,n;(null===(t=v[r])||void 0===t?void 0:t.websocket)&&(null===(n=v[r].websocket)||void 0===n||n.close())},100)}catch(t){}}};return window.addEventListener("terminal-close",n),()=>{window.removeEventListener("terminal-command",t),window.removeEventListener("terminal-close",n)}},[r]),(0,i.useEffect)(()=>{var t;if(!b.current)return;if(null===(t=v[r])||void 0===t?void 0:t.terminal){let{terminal:t,fitAddon:n}=v[r];if(t&&n)try{t.open(b.current),setTimeout(()=>{try{n.fit()}catch(t){console.error("Error fitting terminal:",t)}},100);return}catch(t){console.error("Error reopening terminal:",t),delete v[r]}}let n=new c.Terminal({cursorBlink:!0,fontSize:14,fontFamily:"Menlo, Monaco, Consolas, monospace",theme:{background:"#18181B",foreground:"#FAFAFA",cursor:"#FAFAFA",selectionBackground:"rgba(255, 255, 255, 0.3)"},allowTransparency:!0,scrollback:1e4,convertEol:!0,screenReaderMode:!0,disableStdin:!1,allowProposedApi:!0,rightClickSelectsWord:!0}),o=new s.FitAddon,a=new d.WebLinksAddon;n.loadAddon(o),n.loadAddon(a),n.open(b.current);try{let t=new u.WebglAddon;n.loadAddon(t),t.onContextLoss(()=>{console.warn("WebGL context lost, falling back to canvas renderer"),t.dispose()})}catch(t){console.warn("WebGL not available, falling back to canvas renderer:",t)}setTimeout(()=>{try{o.fit();let{cols:t,rows:r}=n}catch(t){console.error("Error fitting terminal:",t)}},100),setTimeout(()=>{try{o.fit()}catch(t){console.error("Error fitting terminal after delay:",t)}},500),v[r]={terminal:n,websocket:null,fitAddon:o,dataListeners:[]};let l=()=>{try{if(!o)return;if(b.current&&null!==b.current.offsetParent){var t,a,l;o.fit();let{cols:i,rows:c}=n;(null===(a=v[r])||void 0===a?void 0:null===(t=a.websocket)||void 0===t?void 0:t.readyState)===WebSocket.OPEN&&(null===(l=v[r].websocket)||void 0===l||l.send(JSON.stringify({type:"resize",data:{cols:i,rows:c}})))}}catch(t){console.error("Error handling resize:",t)}};window.addEventListener("resize",l);let i=new ResizeObserver(()=>{setTimeout(l,0)});return b.current&&i.observe(b.current),()=>{var t,o;if(window.removeEventListener("resize",l),i&&(b.current&&i.unobserve(b.current),i.disconnect()),n&&n.element&&(null===(t=b.current)||void 0===t?void 0:t.contains(n.element)))try{(null===(o=v[r])||void 0===o?void 0:o.dataListeners)&&(v[r].dataListeners.forEach(t=>{t&&"function"==typeof t.dispose&&t.dispose()}),v[r].dataListeners=[]),n.element.remove()}catch(t){console.error("Error removing terminal element:",t)}}},[r]),(0,i.useEffect)(()=>{let t=setTimeout(()=>{let t=()=>{var t,o,a,l;if(D.current)return;D.current=!0;let i=localStorage.getItem(w),c=f().replace(/^http:\/\//,"")||(k&&S?"".concat(k,":").concat(S):window.location.host),s="https:"===window.location.protocol?"wss://":"ws://",d=i?"".concat(s).concat(c,"/ws?token=").concat(i,"&terminalId=").concat(r):"".concat(s).concat(c,"/ws?terminalId=").concat(r);if((null===(o=v[r])||void 0===o?void 0:null===(t=o.websocket)||void 0===t?void 0:t.readyState)===WebSocket.OPEN){N(!0),D.current=!1;return}if(null===(a=v[r])||void 0===a?void 0:a.websocket)try{let t=v[r].websocket;t&&(t.onclose=null,t.onerror=null,t.onmessage=null,t.onopen=null,(t.readyState===WebSocket.OPEN||t.readyState===WebSocket.CONNECTING)&&t.close()),v[r].websocket=null}catch(t){console.error("Error closing existing websocket:",t)}let u=null===(l=v[r])||void 0===l?void 0:l.terminal;if(!u){D.current=!1;return}u.clear(),u.write("Connecting to terminal...(Attempt ".concat(L.current+1,"/").concat(5,")\r\n"));let m=new WebSocket(d);v[r].websocket=m,j(!1),m.binaryType="arraybuffer";let b=setTimeout(()=>{m.readyState!==WebSocket.OPEN&&(m.close(3001,"连接超时"),D.current=!1)},1e4);m.onopen=()=>{clearTimeout(b),N(!0),j(!1),L.current=0,D.current=!1;try{m.send(JSON.stringify({type:"resize",data:{cols:u.cols,rows:u.rows}}))}catch(t){console.error("Error sending terminal size:",t)}},m.onmessage=t=>{try{if("string"==typeof t.data)try{let n=JSON.parse(t.data);if("auth"===n.type)n.success&&(y(!0),n.token&&(g(n.token),localStorage.setItem(w,n.token)));else if("heartbeat"===n.type);else if("output"===n.type&&n.terminalId===r)try{if(n.data){let t=atob(n.data),r=new Uint8Array(t.length);for(let n=0;n{if(N(!1),1e3===t.code||1001===t.code){u.write("\r\n\uD83D\uDFE0 Connection closed normally\r\n"),D.current=!1;return}u.write("\r\n\uD83D\uDD34 Connection lost (Code: ".concat(t.code,")\r\n")),Date.now()-new Date().getTime()<5e3?setTimeout(()=>{n()},5e3):n()},m.onerror=t=>{if(N(!1),j(!0),console.error("Error handling websocket message:",e),u.write("\r\n\uD83D\uDD34 Connection error\r\n"),m.readyState===WebSocket.OPEN||m.readyState===WebSocket.CONNECTING)try{m.close(3004,"连接错误")}catch(t){}else n()};let p=u.onData(t=>{if(m.readyState===WebSocket.OPEN)try{let r=32>t.charCodeAt(0)||127===t.charCodeAt(0);if("\r"===t){let t=C.current+"\n",r=new TextEncoder().encode(t);m.send(r),u.write("\r\n"),C.current=""}else if("\x7f"===t)C.current.length>0&&(C.current=C.current.slice(0,-1),u.write("\b \b"));else if(r){let r=new TextEncoder().encode(t);m.send(r)}else C.current+=t,u.write(t)}catch(t){console.error("Error sending data to websocket:",t),u.write("\r\nFailed to send data\r\n"),m.readyState!==WebSocket.OPEN&&(u.write("\r\nConnection lost, attempting to reconnect...\r\n"),n())}else m.readyState===WebSocket.CONNECTING?u.write("\r\nConnecting, please wait...\r\n"):(u.write("\r\nConnection lost, attempting to reconnect...\r\n"),n())});return v[r].dataListeners=[p],I.current&&clearInterval(I.current),I.current=setInterval(()=>{if(m.readyState===WebSocket.OPEN)try{m.send(JSON.stringify({type:"heartbeat",data:"ping"}))}catch(t){if(m.readyState===WebSocket.OPEN)try{m.close(3005,"心跳发送失败")}catch(t){}}},5e3),()=>{var t;if(I.current&&(clearInterval(I.current),I.current=null),m.readyState===WebSocket.OPEN||m.readyState===WebSocket.CONNECTING)try{m.close()}catch(t){console.error("Error closing websocket:",t)}(null===(t=v[r])||void 0===t?void 0:t.dataListeners)&&(v[r].dataListeners.forEach(t=>{t&&"function"==typeof t.dispose&&t.dispose()}),v[r].dataListeners=[]),v[r].websocket=null}},n=()=>{if(z.current&&(clearTimeout(z.current),z.current=null),L.current>=5){var n;let t=null===(n=v[r])||void 0===n?void 0:n.terminal;t&&t.write("\r\n⚠️ Maximum reconnection attempts reached (".concat(5,"), please press Enter to retry manually\r\n")),D.current=!1;return}L.current++;let o=Math.min(2e3*Math.pow(1.5,L.current-1),3e4);z.current=setTimeout(()=>{var n;if(null===(n=v[r])||void 0===n?void 0:n.websocket)try{let t=v[r].websocket;t&&(t.readyState===WebSocket.OPEN||t.readyState===WebSocket.CONNECTING)&&(t.onclose=null,t.onerror=null,t.close()),v[r].websocket=null}catch(t){console.error("Error closing existing websocket before reconnect:",t)}t()},o+1e3*Math.random())};return t(),()=>{z.current&&(clearTimeout(z.current),z.current=null),I.current&&(clearInterval(I.current),I.current=null)}},500);return()=>{clearTimeout(t)}},[r,k,S]),(0,i.useEffect)(()=>{let t=()=>{if(A.current&&b.current){var t;let n=A.current.clientHeight;A.current.clientWidth;let o=n-40-12;if(W("".concat(Math.max(200,o),"px")),null===(t=v[r])||void 0===t?void 0:t.fitAddon)try{requestAnimationFrame(()=>{var t,n,a;b.current&&(b.current.style.width="100%",b.current.style.height="".concat(Math.max(200,o),"px")),null===(t=v[r].fitAddon)||void 0===t||t.fit();let l=v[r].terminal;l&&(null===(n=v[r].websocket)||void 0===n?void 0:n.readyState)===WebSocket.OPEN&&(null===(a=v[r].websocket)||void 0===a||a.send(JSON.stringify({type:"resize",data:{cols:l.cols,rows:l.rows}})))})}catch(t){console.error("Error fitting terminal after height update:",t)}}},n=setTimeout(()=>{t()},100),o=null,a=()=>{o&&clearTimeout(o),o=setTimeout(()=>{t()},100)};window.addEventListener("resize",a);let l=new ResizeObserver(()=>{a()});return A.current&&l.observe(A.current),()=>{clearTimeout(n),o&&clearTimeout(o),window.removeEventListener("resize",a),l&&A.current&&(l.unobserve(A.current),l.disconnect())}},[r]),(0,o.jsxs)(m.Zb,{ref:A,className:"h-full w-full border-0 rounded-none bg-zinc-900 text-zinc-50 overflow-hidden flex flex-col",children:[(0,o.jsxs)(m.aY,{className:"p-0 flex-1 flex flex-col h-full",children:[(0,o.jsxs)("div",{className:"jsx-128035d7a41d0013 flex items-center justify-between bg-zinc-800 px-4 py-2 h-10",children:[(0,o.jsxs)("div",{className:"jsx-128035d7a41d0013 flex items-center space-x-2",children:[(0,o.jsx)("div",{className:"jsx-128035d7a41d0013 w-3 h-3 rounded-full bg-red-500"}),(0,o.jsx)("div",{className:"jsx-128035d7a41d0013 w-3 h-3 rounded-full bg-yellow-500"}),(0,o.jsx)("div",{className:"jsx-128035d7a41d0013 w-3 h-3 rounded-full bg-green-500"})]}),(0,o.jsxs)("div",{className:"jsx-128035d7a41d0013 text-sm font-medium",children:["Terminal ",r,E&&!P&&(0,o.jsx)("span",{className:"jsx-128035d7a41d0013 ml-2 text-green-400",children:"●"}),!E&&!T&&!P&&(0,o.jsx)("span",{className:"jsx-128035d7a41d0013 ml-2 text-yellow-400",children:"●"}),T&&!P&&(0,o.jsx)("span",{className:"jsx-128035d7a41d0013 ml-2 text-red-400",children:"●"}),P&&(0,o.jsx)("span",{className:"jsx-128035d7a41d0013 ml-2 text-gray-400",children:"Closing..."})]}),(0,o.jsx)("div",{className:"jsx-128035d7a41d0013 w-4"})," "]}),(0,o.jsx)("div",{ref:b,style:{padding:"6px",height:O,maxHeight:"calc(100% - 40px)"},className:"jsx-128035d7a41d0013 flex-1 w-full overflow-hidden terminal-container"})]}),(0,o.jsx)(l(),{id:"128035d7a41d0013",children:".terminal-container{user-select:text!important;-webkit-user-select:text!important;-moz-user-select:text!important;-ms-user-select:text!important;overflow:hidden!important}.terminal-container .xterm{height:100%;padding:0;overflow:hidden!important}.terminal-container .xterm-viewport{overflow-y:scroll!important;overflow-x:hidden!important;scrollbar-width:none!important;-ms-overflow-style:none!important}.terminal-container .xterm-viewport::-webkit-scrollbar{display:none!important;width:0!important;height:0!important}.terminal-container .xterm-screen{height:100%!important;-moz-user-select:text!important;-ms-user-select:text!important;user-select:text!important;-webkit-user-select:text!important;cursor:text}.terminal-container .xterm-selection{opacity:.3;z-index:3}.terminal-container .xterm-rows{overflow:auto!important;-webkit-font-variant-ligatures:none!important;-moz-font-variant-ligatures:none!important;font-variant-ligatures:none!important;white-space:pre}"})]})}}}]); -------------------------------------------------------------------------------- /src/web/out/_next/static/chunks/253-95c9bbffcbc06f61.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[253],{9205:function(e,r,t){t.d(r,{Z:function(){return a}});var o=t(2265);let n=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),l=function(){for(var e=arguments.length,r=Array(e),t=0;t!!e&&""!==e.trim()&&t.indexOf(e)===r).join(" ").trim()};var i={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};let s=(0,o.forwardRef)((e,r)=>{let{color:t="currentColor",size:n=24,strokeWidth:s=2,absoluteStrokeWidth:a,className:d="",children:c,iconNode:u,...p}=e;return(0,o.createElement)("svg",{ref:r,...i,width:n,height:n,stroke:t,strokeWidth:a?24*Number(s)/Number(n):s,className:l("lucide",d),...p},[...u.map(e=>{let[r,t]=e;return(0,o.createElement)(r,t)}),...Array.isArray(c)?c:[c]])}),a=(e,r)=>{let t=(0,o.forwardRef)((t,i)=>{let{className:a,...d}=t;return(0,o.createElement)(s,{ref:i,iconNode:r,className:l("lucide-".concat(n(e)),a),...d})});return t.displayName="".concat(e),t}},8575:function(e,r,t){t.d(r,{F:function(){return l},e:function(){return i}});var o=t(2265);function n(e,r){if("function"==typeof e)return e(r);null!=e&&(e.current=r)}function l(...e){return r=>{let t=!1,o=e.map(e=>{let o=n(e,r);return t||"function"!=typeof o||(t=!0),o});if(t)return()=>{for(let r=0;r{let t=o.forwardRef((e,t)=>{let{asChild:o,...n}=e,s=o?l.g7:r;return"undefined"!=typeof window&&(window[Symbol.for("radix-ui")]=!0),(0,i.jsx)(s,{...n,ref:t})});return t.displayName=`Primitive.${r}`,{...e,[r]:t}},{});function a(e,r){e&&n.flushSync(()=>e.dispatchEvent(r))}},7053:function(e,r,t){t.d(r,{g7:function(){return i}});var o=t(2265),n=t(8575),l=t(7437),i=o.forwardRef((e,r)=>{let{children:t,...n}=e,i=o.Children.toArray(t),a=i.find(d);if(a){let e=a.props.children,t=i.map(r=>r!==a?r:o.Children.count(e)>1?o.Children.only(null):o.isValidElement(e)?e.props.children:null);return(0,l.jsx)(s,{...n,ref:r,children:o.isValidElement(e)?o.cloneElement(e,void 0,t):null})}return(0,l.jsx)(s,{...n,ref:r,children:t})});i.displayName="Slot";var s=o.forwardRef((e,r)=>{let{children:t,...l}=e;if(o.isValidElement(t)){let e,i;let s=(e=Object.getOwnPropertyDescriptor(t.props,"ref")?.get)&&"isReactWarning"in e&&e.isReactWarning?t.ref:(e=Object.getOwnPropertyDescriptor(t,"ref")?.get)&&"isReactWarning"in e&&e.isReactWarning?t.props.ref:t.props.ref||t.ref,a=function(e,r){let t={...r};for(let o in r){let n=e[o],l=r[o];/^on[A-Z]/.test(o)?n&&l?t[o]=(...e)=>{l(...e),n(...e)}:n&&(t[o]=n):"style"===o?t[o]={...n,...l}:"className"===o&&(t[o]=[n,l].filter(Boolean).join(" "))}return{...e,...t}}(l,t.props);return t.type!==o.Fragment&&(a.ref=r?(0,n.F)(r,s):s),o.cloneElement(t,a)}return o.Children.count(t)>1?o.Children.only(null):null});s.displayName="SlotClone";var a=({children:e})=>(0,l.jsx)(l.Fragment,{children:e});function d(e){return o.isValidElement(e)&&e.type===a}},535:function(e,r,t){t.d(r,{j:function(){return i}});var o=t(1994);let n=e=>"boolean"==typeof e?`${e}`:0===e?"0":e,l=o.W,i=(e,r)=>t=>{var o;if((null==r?void 0:r.variants)==null)return l(e,null==t?void 0:t.class,null==t?void 0:t.className);let{variants:i,defaultVariants:s}=r,a=Object.keys(i).map(e=>{let r=null==t?void 0:t[e],o=null==s?void 0:s[e];if(null===r)return null;let l=n(r)||n(o);return i[e][l]}),d=t&&Object.entries(t).reduce((e,r)=>{let[t,o]=r;return void 0===o||(e[t]=o),e},{});return l(e,a,null==r?void 0:null===(o=r.compoundVariants)||void 0===o?void 0:o.reduce((e,r)=>{let{class:t,className:o,...n}=r;return Object.entries(n).every(e=>{let[r,t]=e;return Array.isArray(t)?t.includes({...s,...d}[r]):({...s,...d})[r]===t})?[...e,t,o]:e},[]),null==t?void 0:t.class,null==t?void 0:t.className)}},1994:function(e,r,t){t.d(r,{W:function(){return o}});function o(){for(var e,r,t=0,o="",n=arguments.length;t{let r=s(e),{conflictingClassGroups:t,conflictingClassGroupModifiers:o}=e;return{getClassGroupId:e=>{let t=e.split("-");return""===t[0]&&1!==t.length&&t.shift(),n(t,r)||i(e)},getConflictingClassGroupIds:(e,r)=>{let n=t[e]||[];return r&&o[e]?[...n,...o[e]]:n}}},n=(e,r)=>{if(0===e.length)return r.classGroupId;let t=e[0],o=r.nextPart.get(t),l=o?n(e.slice(1),o):void 0;if(l)return l;if(0===r.validators.length)return;let i=e.join("-");return r.validators.find(({validator:e})=>e(i))?.classGroupId},l=/^\[(.+)\]$/,i=e=>{if(l.test(e)){let r=l.exec(e)[1],t=r?.substring(0,r.indexOf(":"));if(t)return"arbitrary.."+t}},s=e=>{let{theme:r,prefix:t}=e,o={nextPart:new Map,validators:[]};return u(Object.entries(e.classGroups),t).forEach(([e,t])=>{a(t,o,e,r)}),o},a=(e,r,t,o)=>{e.forEach(e=>{if("string"==typeof e){(""===e?r:d(r,e)).classGroupId=t;return}if("function"==typeof e){if(c(e)){a(e(o),r,t,o);return}r.validators.push({validator:e,classGroupId:t});return}Object.entries(e).forEach(([e,n])=>{a(n,d(r,e),t,o)})})},d=(e,r)=>{let t=e;return r.split("-").forEach(e=>{t.nextPart.has(e)||t.nextPart.set(e,{nextPart:new Map,validators:[]}),t=t.nextPart.get(e)}),t},c=e=>e.isThemeGetter,u=(e,r)=>r?e.map(([e,t])=>[e,t.map(e=>"string"==typeof e?r+e:"object"==typeof e?Object.fromEntries(Object.entries(e).map(([e,t])=>[r+e,t])):e)]):e,p=e=>{if(e<1)return{get:()=>void 0,set:()=>{}};let r=0,t=new Map,o=new Map,n=(n,l)=>{t.set(n,l),++r>e&&(r=0,o=t,t=new Map)};return{get(e){let r=t.get(e);return void 0!==r?r:void 0!==(r=o.get(e))?(n(e,r),r):void 0},set(e,r){t.has(e)?t.set(e,r):n(e,r)}}},b=e=>{let{separator:r,experimentalParseClassName:t}=e,o=1===r.length,n=r[0],l=r.length,i=e=>{let t;let i=[],s=0,a=0;for(let d=0;da?t-a:void 0}};return t?e=>t({className:e,parseClassName:i}):i},f=e=>{if(e.length<=1)return e;let r=[],t=[];return e.forEach(e=>{"["===e[0]?(r.push(...t.sort(),e),t=[]):t.push(e)}),r.push(...t.sort()),r},m=e=>({cache:p(e.cacheSize),parseClassName:b(e),...o(e)}),g=/\s+/,h=(e,r)=>{let{parseClassName:t,getClassGroupId:o,getConflictingClassGroupIds:n}=r,l=[],i=e.trim().split(g),s="";for(let e=i.length-1;e>=0;e-=1){let r=i[e],{modifiers:a,hasImportantModifier:d,baseClassName:c,maybePostfixModifierPosition:u}=t(r),p=!!u,b=o(p?c.substring(0,u):c);if(!b){if(!p||!(b=o(c))){s=r+(s.length>0?" "+s:s);continue}p=!1}let m=f(a).join(":"),g=d?m+"!":m,h=g+b;if(l.includes(h))continue;l.push(h);let y=n(b,p);for(let e=0;e0?" "+s:s)}return s};function y(){let e,r,t=0,o="";for(;t{let r;if("string"==typeof e)return e;let t="";for(let o=0;o{let r=r=>r[e]||[];return r.isThemeGetter=!0,r},x=/^\[(?:([a-z-]+):)?(.+)\]$/i,k=/^\d+\/\d+$/,z=new Set(["px","full","screen"]),j=/^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/,C=/\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/,N=/^(rgba?|hsla?|hwb|(ok)?(lab|lch))\(.+\)$/,E=/^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/,S=/^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/,O=e=>R(e)||z.has(e)||k.test(e),P=e=>B(e,"length",D),R=e=>!!e&&!Number.isNaN(Number(e)),W=e=>B(e,"number",R),G=e=>!!e&&Number.isInteger(Number(e)),$=e=>e.endsWith("%")&&R(e.slice(0,-1)),A=e=>x.test(e),M=e=>j.test(e),I=new Set(["length","size","percentage"]),V=e=>B(e,I,T),_=e=>B(e,"position",T),F=new Set(["image","url"]),L=e=>B(e,F,J),Z=e=>B(e,"",H),q=()=>!0,B=(e,r,t)=>{let o=x.exec(e);return!!o&&(o[1]?"string"==typeof r?o[1]===r:r.has(o[1]):t(o[2]))},D=e=>C.test(e)&&!N.test(e),T=()=>!1,H=e=>E.test(e),J=e=>S.test(e),K=function(e,...r){let t,o,n;let l=function(s){return o=(t=m(r.reduce((e,r)=>r(e),e()))).cache.get,n=t.cache.set,l=i,i(s)};function i(e){let r=o(e);if(r)return r;let l=h(e,t);return n(e,l),l}return function(){return l(y.apply(null,arguments))}}(()=>{let e=w("colors"),r=w("spacing"),t=w("blur"),o=w("brightness"),n=w("borderColor"),l=w("borderRadius"),i=w("borderSpacing"),s=w("borderWidth"),a=w("contrast"),d=w("grayscale"),c=w("hueRotate"),u=w("invert"),p=w("gap"),b=w("gradientColorStops"),f=w("gradientColorStopPositions"),m=w("inset"),g=w("margin"),h=w("opacity"),y=w("padding"),v=w("saturate"),x=w("scale"),k=w("sepia"),z=w("skew"),j=w("space"),C=w("translate"),N=()=>["auto","contain","none"],E=()=>["auto","hidden","clip","visible","scroll"],S=()=>["auto",A,r],I=()=>[A,r],F=()=>["",O,P],B=()=>["auto",R,A],D=()=>["bottom","center","left","left-bottom","left-top","right","right-bottom","right-top","top"],T=()=>["solid","dashed","dotted","double","none"],H=()=>["normal","multiply","screen","overlay","darken","lighten","color-dodge","color-burn","hard-light","soft-light","difference","exclusion","hue","saturation","color","luminosity"],J=()=>["start","end","center","between","around","evenly","stretch"],K=()=>["","0",A],Q=()=>["auto","avoid","all","avoid-page","page","left","right","column"],U=()=>[R,A];return{cacheSize:500,separator:":",theme:{colors:[q],spacing:[O,P],blur:["none","",M,A],brightness:U(),borderColor:[e],borderRadius:["none","","full",M,A],borderSpacing:I(),borderWidth:F(),contrast:U(),grayscale:K(),hueRotate:U(),invert:K(),gap:I(),gradientColorStops:[e],gradientColorStopPositions:[$,P],inset:S(),margin:S(),opacity:U(),padding:I(),saturate:U(),scale:U(),sepia:K(),skew:U(),space:I(),translate:I()},classGroups:{aspect:[{aspect:["auto","square","video",A]}],container:["container"],columns:[{columns:[M]}],"break-after":[{"break-after":Q()}],"break-before":[{"break-before":Q()}],"break-inside":[{"break-inside":["auto","avoid","avoid-page","avoid-column"]}],"box-decoration":[{"box-decoration":["slice","clone"]}],box:[{box:["border","content"]}],display:["block","inline-block","inline","flex","inline-flex","table","inline-table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row-group","table-row","flow-root","grid","inline-grid","contents","list-item","hidden"],float:[{float:["right","left","none","start","end"]}],clear:[{clear:["left","right","both","none","start","end"]}],isolation:["isolate","isolation-auto"],"object-fit":[{object:["contain","cover","fill","none","scale-down"]}],"object-position":[{object:[...D(),A]}],overflow:[{overflow:E()}],"overflow-x":[{"overflow-x":E()}],"overflow-y":[{"overflow-y":E()}],overscroll:[{overscroll:N()}],"overscroll-x":[{"overscroll-x":N()}],"overscroll-y":[{"overscroll-y":N()}],position:["static","fixed","absolute","relative","sticky"],inset:[{inset:[m]}],"inset-x":[{"inset-x":[m]}],"inset-y":[{"inset-y":[m]}],start:[{start:[m]}],end:[{end:[m]}],top:[{top:[m]}],right:[{right:[m]}],bottom:[{bottom:[m]}],left:[{left:[m]}],visibility:["visible","invisible","collapse"],z:[{z:["auto",G,A]}],basis:[{basis:S()}],"flex-direction":[{flex:["row","row-reverse","col","col-reverse"]}],"flex-wrap":[{flex:["wrap","wrap-reverse","nowrap"]}],flex:[{flex:["1","auto","initial","none",A]}],grow:[{grow:K()}],shrink:[{shrink:K()}],order:[{order:["first","last","none",G,A]}],"grid-cols":[{"grid-cols":[q]}],"col-start-end":[{col:["auto",{span:["full",G,A]},A]}],"col-start":[{"col-start":B()}],"col-end":[{"col-end":B()}],"grid-rows":[{"grid-rows":[q]}],"row-start-end":[{row:["auto",{span:[G,A]},A]}],"row-start":[{"row-start":B()}],"row-end":[{"row-end":B()}],"grid-flow":[{"grid-flow":["row","col","dense","row-dense","col-dense"]}],"auto-cols":[{"auto-cols":["auto","min","max","fr",A]}],"auto-rows":[{"auto-rows":["auto","min","max","fr",A]}],gap:[{gap:[p]}],"gap-x":[{"gap-x":[p]}],"gap-y":[{"gap-y":[p]}],"justify-content":[{justify:["normal",...J()]}],"justify-items":[{"justify-items":["start","end","center","stretch"]}],"justify-self":[{"justify-self":["auto","start","end","center","stretch"]}],"align-content":[{content:["normal",...J(),"baseline"]}],"align-items":[{items:["start","end","center","baseline","stretch"]}],"align-self":[{self:["auto","start","end","center","stretch","baseline"]}],"place-content":[{"place-content":[...J(),"baseline"]}],"place-items":[{"place-items":["start","end","center","baseline","stretch"]}],"place-self":[{"place-self":["auto","start","end","center","stretch"]}],p:[{p:[y]}],px:[{px:[y]}],py:[{py:[y]}],ps:[{ps:[y]}],pe:[{pe:[y]}],pt:[{pt:[y]}],pr:[{pr:[y]}],pb:[{pb:[y]}],pl:[{pl:[y]}],m:[{m:[g]}],mx:[{mx:[g]}],my:[{my:[g]}],ms:[{ms:[g]}],me:[{me:[g]}],mt:[{mt:[g]}],mr:[{mr:[g]}],mb:[{mb:[g]}],ml:[{ml:[g]}],"space-x":[{"space-x":[j]}],"space-x-reverse":["space-x-reverse"],"space-y":[{"space-y":[j]}],"space-y-reverse":["space-y-reverse"],w:[{w:["auto","min","max","fit","svw","lvw","dvw",A,r]}],"min-w":[{"min-w":[A,r,"min","max","fit"]}],"max-w":[{"max-w":[A,r,"none","full","min","max","fit","prose",{screen:[M]},M]}],h:[{h:[A,r,"auto","min","max","fit","svh","lvh","dvh"]}],"min-h":[{"min-h":[A,r,"min","max","fit","svh","lvh","dvh"]}],"max-h":[{"max-h":[A,r,"min","max","fit","svh","lvh","dvh"]}],size:[{size:[A,r,"auto","min","max","fit"]}],"font-size":[{text:["base",M,P]}],"font-smoothing":["antialiased","subpixel-antialiased"],"font-style":["italic","not-italic"],"font-weight":[{font:["thin","extralight","light","normal","medium","semibold","bold","extrabold","black",W]}],"font-family":[{font:[q]}],"fvn-normal":["normal-nums"],"fvn-ordinal":["ordinal"],"fvn-slashed-zero":["slashed-zero"],"fvn-figure":["lining-nums","oldstyle-nums"],"fvn-spacing":["proportional-nums","tabular-nums"],"fvn-fraction":["diagonal-fractions","stacked-fractions"],tracking:[{tracking:["tighter","tight","normal","wide","wider","widest",A]}],"line-clamp":[{"line-clamp":["none",R,W]}],leading:[{leading:["none","tight","snug","normal","relaxed","loose",O,A]}],"list-image":[{"list-image":["none",A]}],"list-style-type":[{list:["none","disc","decimal",A]}],"list-style-position":[{list:["inside","outside"]}],"placeholder-color":[{placeholder:[e]}],"placeholder-opacity":[{"placeholder-opacity":[h]}],"text-alignment":[{text:["left","center","right","justify","start","end"]}],"text-color":[{text:[e]}],"text-opacity":[{"text-opacity":[h]}],"text-decoration":["underline","overline","line-through","no-underline"],"text-decoration-style":[{decoration:[...T(),"wavy"]}],"text-decoration-thickness":[{decoration:["auto","from-font",O,P]}],"underline-offset":[{"underline-offset":["auto",O,A]}],"text-decoration-color":[{decoration:[e]}],"text-transform":["uppercase","lowercase","capitalize","normal-case"],"text-overflow":["truncate","text-ellipsis","text-clip"],"text-wrap":[{text:["wrap","nowrap","balance","pretty"]}],indent:[{indent:I()}],"vertical-align":[{align:["baseline","top","middle","bottom","text-top","text-bottom","sub","super",A]}],whitespace:[{whitespace:["normal","nowrap","pre","pre-line","pre-wrap","break-spaces"]}],break:[{break:["normal","words","all","keep"]}],hyphens:[{hyphens:["none","manual","auto"]}],content:[{content:["none",A]}],"bg-attachment":[{bg:["fixed","local","scroll"]}],"bg-clip":[{"bg-clip":["border","padding","content","text"]}],"bg-opacity":[{"bg-opacity":[h]}],"bg-origin":[{"bg-origin":["border","padding","content"]}],"bg-position":[{bg:[...D(),_]}],"bg-repeat":[{bg:["no-repeat",{repeat:["","x","y","round","space"]}]}],"bg-size":[{bg:["auto","cover","contain",V]}],"bg-image":[{bg:["none",{"gradient-to":["t","tr","r","br","b","bl","l","tl"]},L]}],"bg-color":[{bg:[e]}],"gradient-from-pos":[{from:[f]}],"gradient-via-pos":[{via:[f]}],"gradient-to-pos":[{to:[f]}],"gradient-from":[{from:[b]}],"gradient-via":[{via:[b]}],"gradient-to":[{to:[b]}],rounded:[{rounded:[l]}],"rounded-s":[{"rounded-s":[l]}],"rounded-e":[{"rounded-e":[l]}],"rounded-t":[{"rounded-t":[l]}],"rounded-r":[{"rounded-r":[l]}],"rounded-b":[{"rounded-b":[l]}],"rounded-l":[{"rounded-l":[l]}],"rounded-ss":[{"rounded-ss":[l]}],"rounded-se":[{"rounded-se":[l]}],"rounded-ee":[{"rounded-ee":[l]}],"rounded-es":[{"rounded-es":[l]}],"rounded-tl":[{"rounded-tl":[l]}],"rounded-tr":[{"rounded-tr":[l]}],"rounded-br":[{"rounded-br":[l]}],"rounded-bl":[{"rounded-bl":[l]}],"border-w":[{border:[s]}],"border-w-x":[{"border-x":[s]}],"border-w-y":[{"border-y":[s]}],"border-w-s":[{"border-s":[s]}],"border-w-e":[{"border-e":[s]}],"border-w-t":[{"border-t":[s]}],"border-w-r":[{"border-r":[s]}],"border-w-b":[{"border-b":[s]}],"border-w-l":[{"border-l":[s]}],"border-opacity":[{"border-opacity":[h]}],"border-style":[{border:[...T(),"hidden"]}],"divide-x":[{"divide-x":[s]}],"divide-x-reverse":["divide-x-reverse"],"divide-y":[{"divide-y":[s]}],"divide-y-reverse":["divide-y-reverse"],"divide-opacity":[{"divide-opacity":[h]}],"divide-style":[{divide:T()}],"border-color":[{border:[n]}],"border-color-x":[{"border-x":[n]}],"border-color-y":[{"border-y":[n]}],"border-color-s":[{"border-s":[n]}],"border-color-e":[{"border-e":[n]}],"border-color-t":[{"border-t":[n]}],"border-color-r":[{"border-r":[n]}],"border-color-b":[{"border-b":[n]}],"border-color-l":[{"border-l":[n]}],"divide-color":[{divide:[n]}],"outline-style":[{outline:["",...T()]}],"outline-offset":[{"outline-offset":[O,A]}],"outline-w":[{outline:[O,P]}],"outline-color":[{outline:[e]}],"ring-w":[{ring:F()}],"ring-w-inset":["ring-inset"],"ring-color":[{ring:[e]}],"ring-opacity":[{"ring-opacity":[h]}],"ring-offset-w":[{"ring-offset":[O,P]}],"ring-offset-color":[{"ring-offset":[e]}],shadow:[{shadow:["","inner","none",M,Z]}],"shadow-color":[{shadow:[q]}],opacity:[{opacity:[h]}],"mix-blend":[{"mix-blend":[...H(),"plus-lighter","plus-darker"]}],"bg-blend":[{"bg-blend":H()}],filter:[{filter:["","none"]}],blur:[{blur:[t]}],brightness:[{brightness:[o]}],contrast:[{contrast:[a]}],"drop-shadow":[{"drop-shadow":["","none",M,A]}],grayscale:[{grayscale:[d]}],"hue-rotate":[{"hue-rotate":[c]}],invert:[{invert:[u]}],saturate:[{saturate:[v]}],sepia:[{sepia:[k]}],"backdrop-filter":[{"backdrop-filter":["","none"]}],"backdrop-blur":[{"backdrop-blur":[t]}],"backdrop-brightness":[{"backdrop-brightness":[o]}],"backdrop-contrast":[{"backdrop-contrast":[a]}],"backdrop-grayscale":[{"backdrop-grayscale":[d]}],"backdrop-hue-rotate":[{"backdrop-hue-rotate":[c]}],"backdrop-invert":[{"backdrop-invert":[u]}],"backdrop-opacity":[{"backdrop-opacity":[h]}],"backdrop-saturate":[{"backdrop-saturate":[v]}],"backdrop-sepia":[{"backdrop-sepia":[k]}],"border-collapse":[{border:["collapse","separate"]}],"border-spacing":[{"border-spacing":[i]}],"border-spacing-x":[{"border-spacing-x":[i]}],"border-spacing-y":[{"border-spacing-y":[i]}],"table-layout":[{table:["auto","fixed"]}],caption:[{caption:["top","bottom"]}],transition:[{transition:["none","all","","colors","opacity","shadow","transform",A]}],duration:[{duration:U()}],ease:[{ease:["linear","in","out","in-out",A]}],delay:[{delay:U()}],animate:[{animate:["none","spin","ping","pulse","bounce",A]}],transform:[{transform:["","gpu","none"]}],scale:[{scale:[x]}],"scale-x":[{"scale-x":[x]}],"scale-y":[{"scale-y":[x]}],rotate:[{rotate:[G,A]}],"translate-x":[{"translate-x":[C]}],"translate-y":[{"translate-y":[C]}],"skew-x":[{"skew-x":[z]}],"skew-y":[{"skew-y":[z]}],"transform-origin":[{origin:["center","top","top-right","right","bottom-right","bottom","bottom-left","left","top-left",A]}],accent:[{accent:["auto",e]}],appearance:[{appearance:["none","auto"]}],cursor:[{cursor:["auto","default","pointer","wait","text","move","help","not-allowed","none","context-menu","progress","cell","crosshair","vertical-text","alias","copy","no-drop","grab","grabbing","all-scroll","col-resize","row-resize","n-resize","e-resize","s-resize","w-resize","ne-resize","nw-resize","se-resize","sw-resize","ew-resize","ns-resize","nesw-resize","nwse-resize","zoom-in","zoom-out",A]}],"caret-color":[{caret:[e]}],"pointer-events":[{"pointer-events":["none","auto"]}],resize:[{resize:["none","y","x",""]}],"scroll-behavior":[{scroll:["auto","smooth"]}],"scroll-m":[{"scroll-m":I()}],"scroll-mx":[{"scroll-mx":I()}],"scroll-my":[{"scroll-my":I()}],"scroll-ms":[{"scroll-ms":I()}],"scroll-me":[{"scroll-me":I()}],"scroll-mt":[{"scroll-mt":I()}],"scroll-mr":[{"scroll-mr":I()}],"scroll-mb":[{"scroll-mb":I()}],"scroll-ml":[{"scroll-ml":I()}],"scroll-p":[{"scroll-p":I()}],"scroll-px":[{"scroll-px":I()}],"scroll-py":[{"scroll-py":I()}],"scroll-ps":[{"scroll-ps":I()}],"scroll-pe":[{"scroll-pe":I()}],"scroll-pt":[{"scroll-pt":I()}],"scroll-pr":[{"scroll-pr":I()}],"scroll-pb":[{"scroll-pb":I()}],"scroll-pl":[{"scroll-pl":I()}],"snap-align":[{snap:["start","end","center","align-none"]}],"snap-stop":[{snap:["normal","always"]}],"snap-type":[{snap:["none","x","y","both"]}],"snap-strictness":[{snap:["mandatory","proximity"]}],touch:[{touch:["auto","none","manipulation"]}],"touch-x":[{"touch-pan":["x","left","right"]}],"touch-y":[{"touch-pan":["y","up","down"]}],"touch-pz":["touch-pinch-zoom"],select:[{select:["none","text","all","auto"]}],"will-change":[{"will-change":["auto","scroll","contents","transform",A]}],fill:[{fill:[e,"none"]}],"stroke-w":[{stroke:[O,P,W]}],stroke:[{stroke:[e,"none"]}],sr:["sr-only","not-sr-only"],"forced-color-adjust":[{"forced-color-adjust":["auto","none"]}]},conflictingClassGroups:{overflow:["overflow-x","overflow-y"],overscroll:["overscroll-x","overscroll-y"],inset:["inset-x","inset-y","start","end","top","right","bottom","left"],"inset-x":["right","left"],"inset-y":["top","bottom"],flex:["basis","grow","shrink"],gap:["gap-x","gap-y"],p:["px","py","ps","pe","pt","pr","pb","pl"],px:["pr","pl"],py:["pt","pb"],m:["mx","my","ms","me","mt","mr","mb","ml"],mx:["mr","ml"],my:["mt","mb"],size:["w","h"],"font-size":["leading"],"fvn-normal":["fvn-ordinal","fvn-slashed-zero","fvn-figure","fvn-spacing","fvn-fraction"],"fvn-ordinal":["fvn-normal"],"fvn-slashed-zero":["fvn-normal"],"fvn-figure":["fvn-normal"],"fvn-spacing":["fvn-normal"],"fvn-fraction":["fvn-normal"],"line-clamp":["display","overflow"],rounded:["rounded-s","rounded-e","rounded-t","rounded-r","rounded-b","rounded-l","rounded-ss","rounded-se","rounded-ee","rounded-es","rounded-tl","rounded-tr","rounded-br","rounded-bl"],"rounded-s":["rounded-ss","rounded-es"],"rounded-e":["rounded-se","rounded-ee"],"rounded-t":["rounded-tl","rounded-tr"],"rounded-r":["rounded-tr","rounded-br"],"rounded-b":["rounded-br","rounded-bl"],"rounded-l":["rounded-tl","rounded-bl"],"border-spacing":["border-spacing-x","border-spacing-y"],"border-w":["border-w-s","border-w-e","border-w-t","border-w-r","border-w-b","border-w-l"],"border-w-x":["border-w-r","border-w-l"],"border-w-y":["border-w-t","border-w-b"],"border-color":["border-color-s","border-color-e","border-color-t","border-color-r","border-color-b","border-color-l"],"border-color-x":["border-color-r","border-color-l"],"border-color-y":["border-color-t","border-color-b"],"scroll-m":["scroll-mx","scroll-my","scroll-ms","scroll-me","scroll-mt","scroll-mr","scroll-mb","scroll-ml"],"scroll-mx":["scroll-mr","scroll-ml"],"scroll-my":["scroll-mt","scroll-mb"],"scroll-p":["scroll-px","scroll-py","scroll-ps","scroll-pe","scroll-pt","scroll-pr","scroll-pb","scroll-pl"],"scroll-px":["scroll-pr","scroll-pl"],"scroll-py":["scroll-pt","scroll-pb"],touch:["touch-x","touch-y","touch-pz"],"touch-x":["touch"],"touch-y":["touch"],"touch-pz":["touch"]},conflictingClassGroupModifiers:{"font-size":["leading"]}}})}}]); -------------------------------------------------------------------------------- /src/web/out/_next/static/chunks/747-b7871a7d8b45f8b8.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[747],{2489:function(e,t,n){n.d(t,{Z:function(){return r}});let r=(0,n(9205).Z)("X",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]])},6741:function(e,t,n){n.d(t,{M:function(){return r}});function r(e,t,{checkForDefaultPrevented:n=!0}={}){return function(r){if(e?.(r),!1===n||!r.defaultPrevented)return t?.(r)}}},7822:function(e,t,n){n.d(t,{B:function(){return s}});var r=n(2265),u=n(3966),o=n(8575),i=n(7053),l=n(7437);function s(e){let t=e+"CollectionProvider",[n,s]=(0,u.b)(t),[a,c]=n(t,{collectionRef:{current:null},itemMap:new Map}),d=e=>{let{scope:t,children:n}=e,u=r.useRef(null),o=r.useRef(new Map).current;return(0,l.jsx)(a,{scope:t,itemMap:o,collectionRef:u,children:n})};d.displayName=t;let f=e+"CollectionSlot",v=r.forwardRef((e,t)=>{let{scope:n,children:r}=e,u=c(f,n),s=(0,o.e)(t,u.collectionRef);return(0,l.jsx)(i.g7,{ref:s,children:r})});v.displayName=f;let m=e+"CollectionItemSlot",p="data-radix-collection-item",y=r.forwardRef((e,t)=>{let{scope:n,children:u,...s}=e,a=r.useRef(null),d=(0,o.e)(t,a),f=c(m,n);return r.useEffect(()=>(f.itemMap.set(a,{ref:a,...s}),()=>void f.itemMap.delete(a))),(0,l.jsx)(i.g7,{[p]:"",ref:d,children:u})});return y.displayName=m,[{Provider:d,Slot:v,ItemSlot:y},function(t){let n=c(e+"CollectionConsumer",t);return r.useCallback(()=>{let e=n.collectionRef.current;if(!e)return[];let t=Array.from(e.querySelectorAll("[".concat(p,"]")));return Array.from(n.itemMap.values()).sort((e,n)=>t.indexOf(e.ref.current)-t.indexOf(n.ref.current))},[n.collectionRef,n.itemMap])},s]}},3966:function(e,t,n){n.d(t,{b:function(){return i},k:function(){return o}});var r=n(2265),u=n(7437);function o(e,t){let n=r.createContext(t),o=e=>{let{children:t,...o}=e,i=r.useMemo(()=>o,Object.values(o));return(0,u.jsx)(n.Provider,{value:i,children:t})};return o.displayName=e+"Provider",[o,function(u){let o=r.useContext(n);if(o)return o;if(void 0!==t)return t;throw Error(`\`${u}\` must be used within \`${e}\``)}]}function i(e,t=[]){let n=[],o=()=>{let t=n.map(e=>r.createContext(e));return function(n){let u=n?.[e]||t;return r.useMemo(()=>({[`__scope${e}`]:{...n,[e]:u}}),[n,u])}};return o.scopeName=e,[function(t,o){let i=r.createContext(o),l=n.length;n=[...n,o];let s=t=>{let{scope:n,children:o,...s}=t,a=n?.[e]?.[l]||i,c=r.useMemo(()=>s,Object.values(s));return(0,u.jsx)(a.Provider,{value:c,children:o})};return s.displayName=t+"Provider",[s,function(n,u){let s=u?.[e]?.[l]||i,a=r.useContext(s);if(a)return a;if(void 0!==o)return o;throw Error(`\`${n}\` must be used within \`${t}\``)}]},function(...e){let t=e[0];if(1===e.length)return t;let n=()=>{let n=e.map(e=>({useScope:e(),scopeName:e.scopeName}));return function(e){let u=n.reduce((t,{useScope:n,scopeName:r})=>{let u=n(e)[`__scope${r}`];return{...t,...u}},{});return r.useMemo(()=>({[`__scope${t.scopeName}`]:u}),[u])}};return n.scopeName=t.scopeName,n}(o,...t)]}},5278:function(e,t,n){n.d(t,{I0:function(){return E},XB:function(){return f},fC:function(){return y}});var r,u=n(2265),o=n(6741),i=n(6840),l=n(8575),s=n(6606),a=n(7437),c="dismissableLayer.update",d=u.createContext({layers:new Set,layersWithOutsidePointerEventsDisabled:new Set,branches:new Set}),f=u.forwardRef((e,t)=>{var n,f;let{disableOutsidePointerEvents:v=!1,onEscapeKeyDown:y,onPointerDownOutside:E,onFocusOutside:b,onInteractOutside:w,onDismiss:h,...N}=e,C=u.useContext(d),[g,M]=u.useState(null),O=null!==(f=null==g?void 0:g.ownerDocument)&&void 0!==f?f:null===(n=globalThis)||void 0===n?void 0:n.document,[,P]=u.useState({}),R=(0,l.e)(t,e=>M(e)),T=Array.from(C.layers),[L]=[...C.layersWithOutsidePointerEventsDisabled].slice(-1),x=T.indexOf(L),D=g?T.indexOf(g):-1,W=C.layersWithOutsidePointerEventsDisabled.size>0,S=D>=x,k=function(e){var t;let n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null===(t=globalThis)||void 0===t?void 0:t.document,r=(0,s.W)(e),o=u.useRef(!1),i=u.useRef(()=>{});return u.useEffect(()=>{let e=e=>{if(e.target&&!o.current){let t=function(){p("dismissableLayer.pointerDownOutside",r,u,{discrete:!0})},u={originalEvent:e};"touch"===e.pointerType?(n.removeEventListener("click",i.current),i.current=t,n.addEventListener("click",i.current,{once:!0})):t()}else n.removeEventListener("click",i.current);o.current=!1},t=window.setTimeout(()=>{n.addEventListener("pointerdown",e)},0);return()=>{window.clearTimeout(t),n.removeEventListener("pointerdown",e),n.removeEventListener("click",i.current)}},[n,r]),{onPointerDownCapture:()=>o.current=!0}}(e=>{let t=e.target,n=[...C.branches].some(e=>e.contains(t));!S||n||(null==E||E(e),null==w||w(e),e.defaultPrevented||null==h||h())},O),_=function(e){var t;let n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null===(t=globalThis)||void 0===t?void 0:t.document,r=(0,s.W)(e),o=u.useRef(!1);return u.useEffect(()=>{let e=e=>{e.target&&!o.current&&p("dismissableLayer.focusOutside",r,{originalEvent:e},{discrete:!1})};return n.addEventListener("focusin",e),()=>n.removeEventListener("focusin",e)},[n,r]),{onFocusCapture:()=>o.current=!0,onBlurCapture:()=>o.current=!1}}(e=>{let t=e.target;[...C.branches].some(e=>e.contains(t))||(null==b||b(e),null==w||w(e),e.defaultPrevented||null==h||h())},O);return!function(e,t=globalThis?.document){let n=(0,s.W)(e);u.useEffect(()=>{let e=e=>{"Escape"===e.key&&n(e)};return t.addEventListener("keydown",e,{capture:!0}),()=>t.removeEventListener("keydown",e,{capture:!0})},[n,t])}(e=>{D!==C.layers.size-1||(null==y||y(e),!e.defaultPrevented&&h&&(e.preventDefault(),h()))},O),u.useEffect(()=>{if(g)return v&&(0===C.layersWithOutsidePointerEventsDisabled.size&&(r=O.body.style.pointerEvents,O.body.style.pointerEvents="none"),C.layersWithOutsidePointerEventsDisabled.add(g)),C.layers.add(g),m(),()=>{v&&1===C.layersWithOutsidePointerEventsDisabled.size&&(O.body.style.pointerEvents=r)}},[g,O,v,C]),u.useEffect(()=>()=>{g&&(C.layers.delete(g),C.layersWithOutsidePointerEventsDisabled.delete(g),m())},[g,C]),u.useEffect(()=>{let e=()=>P({});return document.addEventListener(c,e),()=>document.removeEventListener(c,e)},[]),(0,a.jsx)(i.WV.div,{...N,ref:R,style:{pointerEvents:W?S?"auto":"none":void 0,...e.style},onFocusCapture:(0,o.M)(e.onFocusCapture,_.onFocusCapture),onBlurCapture:(0,o.M)(e.onBlurCapture,_.onBlurCapture),onPointerDownCapture:(0,o.M)(e.onPointerDownCapture,k.onPointerDownCapture)})});f.displayName="DismissableLayer";var v=u.forwardRef((e,t)=>{let n=u.useContext(d),r=u.useRef(null),o=(0,l.e)(t,r);return u.useEffect(()=>{let e=r.current;if(e)return n.branches.add(e),()=>{n.branches.delete(e)}},[n.branches]),(0,a.jsx)(i.WV.div,{...e,ref:o})});function m(){let e=new CustomEvent(c);document.dispatchEvent(e)}function p(e,t,n,r){let{discrete:u}=r,o=n.originalEvent.target,l=new CustomEvent(e,{bubbles:!1,cancelable:!0,detail:n});t&&o.addEventListener(e,t,{once:!0}),u?(0,i.jH)(o,l):o.dispatchEvent(l)}v.displayName="DismissableLayerBranch";var y=f,E=v},3832:function(e,t,n){n.d(t,{h:function(){return s}});var r=n(2265),u=n(4887),o=n(6840),i=n(1188),l=n(7437),s=r.forwardRef((e,t)=>{var n,s;let{container:a,...c}=e,[d,f]=r.useState(!1);(0,i.b)(()=>f(!0),[]);let v=a||d&&(null===(s=globalThis)||void 0===s?void 0:null===(n=s.document)||void 0===n?void 0:n.body);return v?u.createPortal((0,l.jsx)(o.WV.div,{...c,ref:t}),v):null});s.displayName="Portal"},1599:function(e,t,n){n.d(t,{z:function(){return i}});var r=n(2265),u=n(8575),o=n(1188),i=e=>{var t,n;let i,s;let{present:a,children:c}=e,d=function(e){var t,n;let[u,i]=r.useState(),s=r.useRef({}),a=r.useRef(e),c=r.useRef("none"),[d,f]=(t=e?"mounted":"unmounted",n={mounted:{UNMOUNT:"unmounted",ANIMATION_OUT:"unmountSuspended"},unmountSuspended:{MOUNT:"mounted",ANIMATION_END:"unmounted"},unmounted:{MOUNT:"mounted"}},r.useReducer((e,t)=>{let r=n[e][t];return null!=r?r:e},t));return r.useEffect(()=>{let e=l(s.current);c.current="mounted"===d?e:"none"},[d]),(0,o.b)(()=>{let t=s.current,n=a.current;if(n!==e){let r=c.current,u=l(t);e?f("MOUNT"):"none"===u||(null==t?void 0:t.display)==="none"?f("UNMOUNT"):n&&r!==u?f("ANIMATION_OUT"):f("UNMOUNT"),a.current=e}},[e,f]),(0,o.b)(()=>{if(u){var e;let t;let n=null!==(e=u.ownerDocument.defaultView)&&void 0!==e?e:window,r=e=>{let r=l(s.current).includes(e.animationName);if(e.target===u&&r&&(f("ANIMATION_END"),!a.current)){let e=u.style.animationFillMode;u.style.animationFillMode="forwards",t=n.setTimeout(()=>{"forwards"===u.style.animationFillMode&&(u.style.animationFillMode=e)})}},o=e=>{e.target===u&&(c.current=l(s.current))};return u.addEventListener("animationstart",o),u.addEventListener("animationcancel",r),u.addEventListener("animationend",r),()=>{n.clearTimeout(t),u.removeEventListener("animationstart",o),u.removeEventListener("animationcancel",r),u.removeEventListener("animationend",r)}}f("ANIMATION_END")},[u,f]),{isPresent:["mounted","unmountSuspended"].includes(d),ref:r.useCallback(e=>{e&&(s.current=getComputedStyle(e)),i(e)},[])}}(a),f="function"==typeof c?c({present:d.isPresent}):r.Children.only(c),v=(0,u.e)(d.ref,(i=null===(t=Object.getOwnPropertyDescriptor(f.props,"ref"))||void 0===t?void 0:t.get)&&"isReactWarning"in i&&i.isReactWarning?f.ref:(i=null===(n=Object.getOwnPropertyDescriptor(f,"ref"))||void 0===n?void 0:n.get)&&"isReactWarning"in i&&i.isReactWarning?f.props.ref:f.props.ref||f.ref);return"function"==typeof c||d.isPresent?r.cloneElement(f,{ref:v}):null};function l(e){return(null==e?void 0:e.animationName)||"none"}i.displayName="Presence"},6606:function(e,t,n){n.d(t,{W:function(){return u}});var r=n(2265);function u(e){let t=r.useRef(e);return r.useEffect(()=>{t.current=e}),r.useMemo(()=>(...e)=>t.current?.(...e),[])}},886:function(e,t,n){n.d(t,{T:function(){return o}});var r=n(2265),u=n(6606);function o({prop:e,defaultProp:t,onChange:n=()=>{}}){let[o,i]=function({defaultProp:e,onChange:t}){let n=r.useState(e),[o]=n,i=r.useRef(o),l=(0,u.W)(t);return r.useEffect(()=>{i.current!==o&&(l(o),i.current=o)},[o,i,l]),n}({defaultProp:t,onChange:n}),l=void 0!==e,s=l?e:o,a=(0,u.W)(n);return[s,r.useCallback(t=>{if(l){let n="function"==typeof t?t(e):t;n!==e&&a(n)}else i(t)},[l,e,i,a])]}},1188:function(e,t,n){n.d(t,{b:function(){return u}});var r=n(2265),u=globalThis?.document?r.useLayoutEffect:()=>{}},5098:function(e,t,n){n.d(t,{T:function(){return i}});var r=n(2265),u=n(6840),o=n(7437),i=r.forwardRef((e,t)=>(0,o.jsx)(u.WV.span,{...e,ref:t,style:{position:"absolute",border:0,width:1,height:1,padding:0,margin:-1,overflow:"hidden",clip:"rect(0, 0, 0, 0)",whiteSpace:"nowrap",wordWrap:"normal",...e.style}}));i.displayName="VisuallyHidden"}}]); -------------------------------------------------------------------------------- /src/web/out/_next/static/chunks/846.958e4cd3e1795d69.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[846],{4846:function(e,t,a){a.r(t),a.d(t,{CommandInput:function(){return ex}});var n=a(7437),r=a(2265),o=a(5186),s=a(2869),i=a(5974),l=a(9205);let c=(0,l.Z)("Plus",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]]),d=(0,l.Z)("SquarePen",[["path",{d:"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7",key:"1m0v6g"}],["path",{d:"M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z",key:"ohrbg2"}]]),u=(0,l.Z)("Terminal",[["polyline",{points:"4 17 10 11 4 5",key:"akl6gq"}],["line",{x1:"12",x2:"20",y1:"19",y2:"19",key:"q2wloq"}]]),m=(0,l.Z)("Send",[["path",{d:"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z",key:"1ffxy3"}],["path",{d:"m21.854 2.147-10.94 10.939",key:"12cjpa"}]]);var p=a(6741),f=a(8575),h=a(3966),g=a(9255),x=a(886),y=a(5278),v=a(9103),j=a(3832),N=a(1599),w=a(6840),b=a(6097),C=a(9157),k=a(5478),D=a(7053),E="Dialog",[S,T]=(0,h.b)(E),[I,_]=S(E),R=e=>{let{__scopeDialog:t,children:a,open:o,defaultOpen:s,onOpenChange:i,modal:l=!0}=e,c=r.useRef(null),d=r.useRef(null),[u=!1,m]=(0,x.T)({prop:o,defaultProp:s,onChange:i});return(0,n.jsx)(I,{scope:t,triggerRef:c,contentRef:d,contentId:(0,g.M)(),titleId:(0,g.M)(),descriptionId:(0,g.M)(),open:u,onOpenChange:m,onOpenToggle:r.useCallback(()=>m(e=>!e),[m]),modal:l,children:a})};R.displayName=E;var A="DialogTrigger";r.forwardRef((e,t)=>{let{__scopeDialog:a,...r}=e,o=_(A,a),s=(0,f.e)(t,o.triggerRef);return(0,n.jsx)(w.WV.button,{type:"button","aria-haspopup":"dialog","aria-expanded":o.open,"aria-controls":o.contentId,"data-state":Q(o.open),...r,ref:s,onClick:(0,p.M)(e.onClick,o.onOpenToggle)})}).displayName=A;var O="DialogPortal",[M,F]=S(O,{forceMount:void 0}),P=e=>{let{__scopeDialog:t,forceMount:a,children:o,container:s}=e,i=_(O,t);return(0,n.jsx)(M,{scope:t,forceMount:a,children:r.Children.map(o,e=>(0,n.jsx)(N.z,{present:a||i.open,children:(0,n.jsx)(j.h,{asChild:!0,container:s,children:e})}))})};P.displayName=O;var z="DialogOverlay",U=r.forwardRef((e,t)=>{let a=F(z,e.__scopeDialog),{forceMount:r=a.forceMount,...o}=e,s=_(z,e.__scopeDialog);return s.modal?(0,n.jsx)(N.z,{present:r||s.open,children:(0,n.jsx)(V,{...o,ref:t})}):null});U.displayName=z;var V=r.forwardRef((e,t)=>{let{__scopeDialog:a,...r}=e,o=_(z,a);return(0,n.jsx)(C.Z,{as:D.g7,allowPinchZoom:!0,shards:[o.contentRef],children:(0,n.jsx)(w.WV.div,{"data-state":Q(o.open),...r,ref:t,style:{pointerEvents:"auto",...r.style}})})}),B="DialogContent",W=r.forwardRef((e,t)=>{let a=F(B,e.__scopeDialog),{forceMount:r=a.forceMount,...o}=e,s=_(B,e.__scopeDialog);return(0,n.jsx)(N.z,{present:r||s.open,children:s.modal?(0,n.jsx)(Z,{...o,ref:t}):(0,n.jsx)(q,{...o,ref:t})})});W.displayName=B;var Z=r.forwardRef((e,t)=>{let a=_(B,e.__scopeDialog),o=r.useRef(null),s=(0,f.e)(t,a.contentRef,o);return r.useEffect(()=>{let e=o.current;if(e)return(0,k.Ry)(e)},[]),(0,n.jsx)(L,{...e,ref:s,trapFocus:a.open,disableOutsidePointerEvents:!0,onCloseAutoFocus:(0,p.M)(e.onCloseAutoFocus,e=>{var t;e.preventDefault(),null===(t=a.triggerRef.current)||void 0===t||t.focus()}),onPointerDownOutside:(0,p.M)(e.onPointerDownOutside,e=>{let t=e.detail.originalEvent,a=0===t.button&&!0===t.ctrlKey;(2===t.button||a)&&e.preventDefault()}),onFocusOutside:(0,p.M)(e.onFocusOutside,e=>e.preventDefault())})}),q=r.forwardRef((e,t)=>{let a=_(B,e.__scopeDialog),o=r.useRef(!1),s=r.useRef(!1);return(0,n.jsx)(L,{...e,ref:t,trapFocus:!1,disableOutsidePointerEvents:!1,onCloseAutoFocus:t=>{var n,r;null===(n=e.onCloseAutoFocus)||void 0===n||n.call(e,t),t.defaultPrevented||(o.current||null===(r=a.triggerRef.current)||void 0===r||r.focus(),t.preventDefault()),o.current=!1,s.current=!1},onInteractOutside:t=>{var n,r;null===(n=e.onInteractOutside)||void 0===n||n.call(e,t),t.defaultPrevented||(o.current=!0,"pointerdown"!==t.detail.originalEvent.type||(s.current=!0));let i=t.target;(null===(r=a.triggerRef.current)||void 0===r?void 0:r.contains(i))&&t.preventDefault(),"focusin"===t.detail.originalEvent.type&&s.current&&t.preventDefault()}})}),L=r.forwardRef((e,t)=>{let{__scopeDialog:a,trapFocus:o,onOpenAutoFocus:s,onCloseAutoFocus:i,...l}=e,c=_(B,a),d=r.useRef(null),u=(0,f.e)(t,d);return(0,b.EW)(),(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(v.M,{asChild:!0,loop:!0,trapped:o,onMountAutoFocus:s,onUnmountAutoFocus:i,children:(0,n.jsx)(y.XB,{role:"dialog",id:c.contentId,"aria-describedby":c.descriptionId,"aria-labelledby":c.titleId,"data-state":Q(c.open),...l,ref:u,onDismiss:()=>c.onOpenChange(!1)})}),(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(ea,{titleId:c.titleId}),(0,n.jsx)(en,{contentRef:d,descriptionId:c.descriptionId})]})]})}),H="DialogTitle",X=r.forwardRef((e,t)=>{let{__scopeDialog:a,...r}=e,o=_(H,a);return(0,n.jsx)(w.WV.h2,{id:o.titleId,...r,ref:t})});X.displayName=H;var G="DialogDescription",J=r.forwardRef((e,t)=>{let{__scopeDialog:a,...r}=e,o=_(G,a);return(0,n.jsx)(w.WV.p,{id:o.descriptionId,...r,ref:t})});J.displayName=G;var K="DialogClose",Y=r.forwardRef((e,t)=>{let{__scopeDialog:a,...r}=e,o=_(K,a);return(0,n.jsx)(w.WV.button,{type:"button",...r,ref:t,onClick:(0,p.M)(e.onClick,()=>o.onOpenChange(!1))})});function Q(e){return e?"open":"closed"}Y.displayName=K;var $="DialogTitleWarning",[ee,et]=(0,h.k)($,{contentName:B,titleName:H,docsSlug:"dialog"}),ea=e=>{let{titleId:t}=e,a=et($),n="`".concat(a.contentName,"` requires a `").concat(a.titleName,"` for the component to be accessible for screen reader users.\n\nIf you want to hide the `").concat(a.titleName,"`, you can wrap it with our VisuallyHidden component.\n\nFor more information, see https://radix-ui.com/primitives/docs/components/").concat(a.docsSlug);return r.useEffect(()=>{t&&!document.getElementById(t)&&console.error(n)},[n,t]),null},en=e=>{let{contentRef:t,descriptionId:a}=e,n=et("DialogDescriptionWarning"),o="Warning: Missing `Description` or `aria-describedby={undefined}` for {".concat(n.contentName,"}.");return r.useEffect(()=>{var e;let n=null===(e=t.current)||void 0===e?void 0:e.getAttribute("aria-describedby");a&&n&&!document.getElementById(a)&&console.warn(o)},[o,t,a]),null},er=a(2489),eo=a(4508);let es=r.forwardRef((e,t)=>{let{className:a,...r}=e;return(0,n.jsx)(U,{ref:t,className:(0,eo.cn)("fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",a),...r})});es.displayName=U.displayName;let ei=r.forwardRef((e,t)=>{let{className:a,children:r,...o}=e;return(0,n.jsxs)(P,{children:[(0,n.jsx)(es,{}),(0,n.jsxs)(W,{ref:t,className:(0,eo.cn)("fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",a),...o,children:[r,(0,n.jsxs)(Y,{className:"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",children:[(0,n.jsx)(er.Z,{className:"h-4 w-4"}),(0,n.jsx)("span",{className:"sr-only",children:"Close"})]})]})]})});ei.displayName=W.displayName;let el=e=>{let{className:t,...a}=e;return(0,n.jsx)("div",{className:(0,eo.cn)("flex flex-col space-y-1.5 text-center sm:text-left",t),...a})};el.displayName="DialogHeader";let ec=e=>{let{className:t,...a}=e;return(0,n.jsx)("div",{className:(0,eo.cn)("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",t),...a})};ec.displayName="DialogFooter";let ed=r.forwardRef((e,t)=>{let{className:a,...r}=e;return(0,n.jsx)(X,{ref:t,className:(0,eo.cn)("text-lg font-semibold leading-none tracking-tight",a),...r})});ed.displayName=X.displayName;let eu=r.forwardRef((e,t)=>{let{className:a,...r}=e;return(0,n.jsx)(J,{ref:t,className:(0,eo.cn)("text-sm text-muted-foreground",a),...r})});eu.displayName=J.displayName;var em=a(6815);let ep=r.forwardRef((e,t)=>{let{className:a,...r}=e;return(0,n.jsx)("textarea",{className:(0,eo.cn)("flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",a),ref:t,...r})});ep.displayName="Textarea";let ef=(0,l.Z)("Trash2",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",key:"4alrt4"}],["path",{d:"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",key:"v07s0e"}],["line",{x1:"10",x2:"10",y1:"11",y2:"17",key:"1uufr5"}],["line",{x1:"14",x2:"14",y1:"11",y2:"17",key:"xtxkd"}]]);function eh(e){let{open:t,onOpenChange:a,onSave:i,onDelete:l,editCommand:c,title:d="Add Command"}=e,[u,m]=(0,r.useState)(""),[p,f]=(0,r.useState)(""),[h,g]=(0,r.useState)(""),[x,y]=(0,r.useState)(!1),[v,j]=(0,r.useState)(!1);(0,r.useEffect)(()=>{c?(m(c.name),f(c.command),g(c.description||"")):(m(""),f(""),g(""))},[c,t]);let N=async()=>{if(u.trim()&&p.trim()){y(!0);try{await i(u.trim(),p.trim(),h.trim()),a(!1)}catch(e){}finally{y(!1)}}},w=async()=>{if(c&&l){j(!0);try{await l(c),a(!1)}catch(e){}finally{j(!1)}}};return(0,n.jsx)(R,{open:t,onOpenChange:a,children:(0,n.jsxs)(ei,{className:"sm:max-w-[425px]",children:[(0,n.jsxs)(el,{children:[(0,n.jsx)(ed,{children:d}),(0,n.jsx)(eu,{children:"Add or edit your commands. You can quickly use these commands after creation."})]}),(0,n.jsxs)("div",{className:"grid gap-4 py-4",children:[(0,n.jsxs)("div",{className:"grid grid-cols-4 items-center gap-4",children:[(0,n.jsx)(em._,{htmlFor:"name",className:"text-right",children:"Name"}),(0,n.jsx)(o.I,{id:"name",value:u,onChange:e=>m(e.target.value),className:"col-span-3",disabled:!!c,placeholder:"Example: View processes"})]}),(0,n.jsxs)("div",{className:"grid grid-cols-4 items-center gap-4",children:[(0,n.jsx)(em._,{htmlFor:"command",className:"text-right",children:"Command"}),(0,n.jsx)(o.I,{id:"command",value:p,onChange:e=>f(e.target.value),className:"col-span-3",placeholder:"Example: ps aux"})]}),(0,n.jsxs)("div",{className:"grid grid-cols-4 items-center gap-4",children:[(0,n.jsx)(em._,{htmlFor:"description",className:"text-right",children:"Description"}),(0,n.jsx)(ep,{id:"description",value:h,onChange:e=>g(e.target.value),className:"col-span-3",placeholder:"Description of command usage (optional)",rows:3})]})]}),(0,n.jsxs)(ec,{className:"flex items-center justify-between",children:[c&&l&&(0,n.jsxs)(s.z,{type:"button",variant:"destructive",onClick:w,disabled:v||x,className:"mr-auto",children:[(0,n.jsx)(ef,{className:"h-4 w-4 mr-2"}),v?"Deleting...":"Delete"]}),(0,n.jsx)(s.z,{type:"submit",onClick:N,disabled:!u.trim()||!p.trim()||x||v,children:x?"Saving...":"Save Command"})]})]})})}var eg=a(7992);function ex(e){let{config:t,onSubmit:a}=e,[l,p]=(0,r.useState)(""),[f,h]=(0,r.useState)(!1),[g,x]=(0,r.useState)(void 0),{commands:y,loading:v,error:j,addCommand:N,updateCommand:w,deleteCommand:b,initialized:C}=function(e){let[t,a]=(0,r.useState)([]),[n,o]=(0,r.useState)(!1),[s,i]=(0,r.useState)(null),[l,c]=(0,r.useState)(!1),{toast:d}=(0,eg.pm)(),u=(0,r.useRef)(0),m=(0,r.useCallback)(t=>e&&e.ip&&e.port?"http://".concat(e.ip,":").concat(e.port).concat(t):window.env&&window.env.NEXT_PUBLIC_API_URL?"".concat(window.env.NEXT_PUBLIC_API_URL).concat(t):t,[e]),p=(0,r.useCallback)(()=>localStorage.getItem("terminal_auth_token"),[]),f=(0,r.useCallback)(async()=>{if(n||u.current>=3)return;u.current+=1,o(!0),i(null);let e=p();if(!e){o(!1),c(!0);return}try{let t=m("/api/commands"),n=await fetch(t,{headers:{"Content-Type":"application/json",Authorization:"Bearer ".concat(e)}});if(!n.ok){let e="Failed to fetch commands";try{let t=await n.json();e=t.error||t.message||e}catch(t){e="".concat(e,": ").concat(n.statusText)}throw Error(e)}let r=await n.json();a(r.data||[]),u.current=0}catch(t){let e=t instanceof Error?t.message:"Error occurred while fetching commands";i(e),1===u.current&&d({title:"Failed to fetch commands",description:e,variant:"destructive"})}finally{o(!1),c(!0)}},[d,m,p]),h=(0,r.useCallback)(async(e,t,a)=>{if(n)return;o(!0),i(null);let r=p();if(!r){i("Not logged in, please login first"),o(!1);return}try{let n=m("/api/commands/add"),o=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json",Authorization:"Bearer ".concat(r)},body:JSON.stringify({name:e,command:t,description:a})});if(!o.ok){let e="Failed to add command";try{let t=await o.json();e=t.error||t.message||e}catch(t){e="".concat(e,": ").concat(o.statusText)}throw Error(e)}d({title:"Add successful",description:'Command "'.concat(e,'" has been added')}),u.current=0,await f()}catch(t){let e=t instanceof Error?t.message:"Error occurred while adding command";i(e),d({title:"Failed to add command",description:e,variant:"destructive"})}finally{o(!1)}},[f,d,m,p]),g=(0,r.useCallback)(async(e,t,a)=>{if(n)return;o(!0),i(null);let r=p();if(!r){i("Not logged in, please login first"),o(!1);return}try{let n=m("/api/commands/update"),o=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json",Authorization:"Bearer ".concat(r)},body:JSON.stringify({name:e,command:t,description:a})});if(!o.ok){let e="Failed to update command";try{let t=await o.json();e=t.error||t.message||e}catch(t){e="".concat(e,": ").concat(o.statusText)}throw Error(e)}d({title:"Update successful",description:'Command "'.concat(e,'" has been updated')}),u.current=0,await f()}catch(t){let e=t instanceof Error?t.message:"Error occurred while updating command";i(e),d({title:"Failed to update command",description:e,variant:"destructive"})}finally{o(!1)}},[f,d,m,p]),x=(0,r.useCallback)(async e=>{if(n)return;o(!0),i(null);let t=p();if(!t){i("Not logged in, please login first"),o(!1);return}try{let a=m("/api/commands/delete?name=".concat(encodeURIComponent(e))),n=await fetch(a,{method:"GET",headers:{Authorization:"Bearer ".concat(t)}});if(!n.ok){let e="Failed to delete command";try{let t=await n.json();e=t.error||t.message||e}catch(t){e="".concat(e,": ").concat(n.statusText)}throw Error(e)}d({title:"Delete successful",description:'Command "'.concat(e,'" has been deleted')}),u.current=0,await f()}catch(t){let e=t instanceof Error?t.message:"Error occurred while deleting command";i(e),d({title:"Failed to delete command",description:e,variant:"destructive"})}finally{o(!1)}},[f,d,m,p]);return(0,r.useEffect)(()=>{l||f()},[null==e?void 0:e.ip,null==e?void 0:e.port,l,f]),{commands:t,loading:n,error:s,fetchCommands:f,addCommand:h,updateCommand:g,deleteCommand:x,initialized:l}}({ip:t.ip,port:t.port}),{toast:k}=(0,eg.pm)(),D=()=>{l.trim()&&(a(l),p(""))},E=e=>{p(t=>t.endsWith(" ")||""===t?t+e:t+" "+e)},S=()=>{x(void 0),h(!0)},T=e=>{x(e),h(!0)},I=async(e,t,a)=>{try{g?await w(e,t,a):await N(e,t,a)}catch(e){}},_=async e=>{try{await b(e.name),k({title:"Delete successful",description:'Command "'.concat(e.name,'" has been deleted')})}catch(e){k({title:"Delete failed",description:"Unable to delete command, please try again later",variant:"destructive"})}};return(0,n.jsxs)("div",{className:"space-y-4",children:[v&&!C?(0,n.jsx)("div",{className:"text-center py-2 text-muted-foreground",children:"Loading commands..."}):C&&0===y.length?(0,n.jsx)("div",{className:"flex items-center justify-center py-1 gap-2",children:(0,n.jsx)(i.C,{variant:"outline",className:"cursor-pointer hover:bg-primary hover:text-primary-foreground transition-colors",children:(0,n.jsxs)("div",{onClick:S,className:"flex items-center",children:[(0,n.jsx)(c,{className:"h-3 w-3 mr-1"}),"Add Command"]})})}):(0,n.jsxs)("div",{className:"flex flex-wrap gap-2 mb-2",children:[y.map(e=>(0,n.jsx)("div",{className:"flex items-center",children:(0,n.jsx)(i.C,{variant:"outline",className:"cursor-pointer hover:bg-primary hover:text-primary-foreground transition-colors group",children:(0,n.jsxs)("div",{onClick:()=>E(e.command),className:"flex items-center",children:[e.name,(0,n.jsx)(s.z,{variant:"ghost",size:"icon",className:"h-4 w-4 ml-1 opacity-0 group-hover:opacity-100",onClick:t=>{t.stopPropagation(),T(e)},children:(0,n.jsx)(d,{className:"h-3 w-3"})})]})})},e.id)),(0,n.jsx)(i.C,{variant:"outline",className:"cursor-pointer hover:bg-primary hover:text-primary-foreground transition-colors",children:(0,n.jsxs)("div",{onClick:S,className:"flex items-center",children:[(0,n.jsx)(c,{className:"h-3 w-3 mr-1"}),"Add Command"]})})]}),(0,n.jsxs)("div",{className:"flex gap-2",children:[(0,n.jsx)(o.I,{value:l,onChange:e=>p(e.target.value),onKeyDown:e=>{"Enter"===e.key&&D()},placeholder:"Enter command...",className:"flex-1"}),(0,n.jsxs)(s.z,{onClick:()=>{if(!l.trim()){k({title:"Command is empty",description:"Please enter a command to convert",variant:"destructive"});return}try{let e=btoa(l);p('echo "'.concat(e,'" | base64 -d | bash')),k({title:"Conversion successful",description:"Command has been converted to base64 format"})}catch(e){k({title:"Conversion failed",description:"Unable to convert command, please check your input",variant:"destructive"})}},className:"flex items-center gap-2",variant:"outline",title:"Convert to base64 format command",children:[(0,n.jsx)(u,{className:"h-4 w-4"}),"Base64"]}),(0,n.jsxs)(s.z,{onClick:D,className:"flex items-center gap-2",children:[(0,n.jsx)(m,{className:"h-4 w-4"}),"Send"]})]}),(0,n.jsx)(eh,{open:f,onOpenChange:h,onSave:I,onDelete:_,editCommand:g,title:g?"Edit Command":"Add Command"})]})}},7992:function(e,t,a){a.d(t,{pm:function(){return m}});var n=a(2265);let r=0,o=new Map,s=e=>{if(o.has(e))return;let t=setTimeout(()=>{o.delete(e),d({type:"REMOVE_TOAST",toastId:e})},1e6);o.set(e,t)},i=(e,t)=>{switch(t.type){case"ADD_TOAST":return{...e,toasts:[t.toast,...e.toasts].slice(0,1)};case"UPDATE_TOAST":return{...e,toasts:e.toasts.map(e=>e.id===t.toast.id?{...e,...t.toast}:e)};case"DISMISS_TOAST":{let{toastId:a}=t;return a?s(a):e.toasts.forEach(e=>{s(e.id)}),{...e,toasts:e.toasts.map(e=>e.id===a||void 0===a?{...e,open:!1}:e)}}case"REMOVE_TOAST":if(void 0===t.toastId)return{...e,toasts:[]};return{...e,toasts:e.toasts.filter(e=>e.id!==t.toastId)}}},l=[],c={toasts:[]};function d(e){c=i(c,e),l.forEach(e=>{e(c)})}function u(e){let{...t}=e,a=(r=(r+1)%Number.MAX_SAFE_INTEGER).toString(),n=()=>d({type:"DISMISS_TOAST",toastId:a});return d({type:"ADD_TOAST",toast:{...t,id:a,open:!0,onOpenChange:e=>{e||n()}}}),{id:a,dismiss:n,update:e=>d({type:"UPDATE_TOAST",toast:{...e,id:a}})}}function m(){let[e,t]=n.useState(c);return n.useEffect(()=>(l.push(t),()=>{let e=l.indexOf(t);e>-1&&l.splice(e,1)}),[e]),{...e,toast:u,dismiss:e=>d({type:"DISMISS_TOAST",toastId:e})}}}}]); -------------------------------------------------------------------------------- /src/web/out/_next/static/chunks/app/_not-found/page-6d6f02352e3a435e.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[409],{7589:function(e,t,n){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_not-found/page",function(){return n(3634)}])},3634:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return s}}),n(7043);let i=n(7437);n(2265);let o={fontFamily:'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',height:"100vh",textAlign:"center",display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center"},l={display:"inline-block"},r={display:"inline-block",margin:"0 20px 0 0",padding:"0 23px 0 0",fontSize:24,fontWeight:500,verticalAlign:"top",lineHeight:"49px"},d={fontSize:14,fontWeight:400,lineHeight:"49px",margin:0};function s(){return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)("title",{children:"404: This page could not be found."}),(0,i.jsx)("div",{style:o,children:(0,i.jsxs)("div",{children:[(0,i.jsx)("style",{dangerouslySetInnerHTML:{__html:"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}),(0,i.jsx)("h1",{className:"next-error-h1",style:r,children:"404"}),(0,i.jsx)("div",{style:l,children:(0,i.jsx)("h2",{style:d,children:"This page could not be found."})})]})})]})}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)}},function(e){e.O(0,[971,117,744],function(){return e(e.s=7589)}),_N_E=e.O()}]); -------------------------------------------------------------------------------- /src/web/out/_next/static/chunks/app/layout-2494ccb28bcae9b7.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[185],{9162:function(e,t,r){Promise.resolve().then(r.bind(r,4694)),Promise.resolve().then(r.t.bind(r,911,23)),Promise.resolve().then(r.t.bind(r,7960,23))},4694:function(e,t,r){"use strict";r.d(t,{Toaster:function(){return ea}});var n=r(7437),o=r(2265),s=r(4887),a=r(6741),i=r(8575),l=r(7822),d=r(3966),u=r(5278),c=r(3832),f=r(1599),p=r(6840),v=r(6606),m=r(886),w=r(1188),x=r(5098),y="ToastProvider",[g,h,E]=(0,l.B)("Toast"),[T,b]=(0,d.b)("Toast",[E]),[N,R]=T(y),j=e=>{let{__scopeToast:t,label:r="Notification",duration:s=5e3,swipeDirection:a="right",swipeThreshold:i=50,children:l}=e,[d,u]=o.useState(null),[c,f]=o.useState(0),p=o.useRef(!1),v=o.useRef(!1);return r.trim()||console.error("Invalid prop `label` supplied to `".concat(y,"`. Expected non-empty `string`.")),(0,n.jsx)(g.Provider,{scope:t,children:(0,n.jsx)(N,{scope:t,label:r,duration:s,swipeDirection:a,swipeThreshold:i,toastCount:c,viewport:d,onViewportChange:u,onToastAdd:o.useCallback(()=>f(e=>e+1),[]),onToastRemove:o.useCallback(()=>f(e=>e-1),[]),isFocusedToastEscapeKeyDownRef:p,isClosePausedRef:v,children:l})})};j.displayName=y;var P="ToastViewport",S=["F8"],C="toast.viewportPause",D="toast.viewportResume",_=o.forwardRef((e,t)=>{let{__scopeToast:r,hotkey:s=S,label:a="Notifications ({hotkey})",...l}=e,d=R(P,r),c=h(r),f=o.useRef(null),v=o.useRef(null),m=o.useRef(null),w=o.useRef(null),x=(0,i.e)(t,w,d.onViewportChange),y=s.join("+").replace(/Key/g,"").replace(/Digit/g,""),E=d.toastCount>0;o.useEffect(()=>{let e=e=>{var t;0!==s.length&&s.every(t=>e[t]||e.code===t)&&(null===(t=w.current)||void 0===t||t.focus())};return document.addEventListener("keydown",e),()=>document.removeEventListener("keydown",e)},[s]),o.useEffect(()=>{let e=f.current,t=w.current;if(E&&e&&t){let r=()=>{if(!d.isClosePausedRef.current){let e=new CustomEvent(C);t.dispatchEvent(e),d.isClosePausedRef.current=!0}},n=()=>{if(d.isClosePausedRef.current){let e=new CustomEvent(D);t.dispatchEvent(e),d.isClosePausedRef.current=!1}},o=t=>{e.contains(t.relatedTarget)||n()},s=()=>{e.contains(document.activeElement)||n()};return e.addEventListener("focusin",r),e.addEventListener("focusout",o),e.addEventListener("pointermove",r),e.addEventListener("pointerleave",s),window.addEventListener("blur",r),window.addEventListener("focus",n),()=>{e.removeEventListener("focusin",r),e.removeEventListener("focusout",o),e.removeEventListener("pointermove",r),e.removeEventListener("pointerleave",s),window.removeEventListener("blur",r),window.removeEventListener("focus",n)}}},[E,d.isClosePausedRef]);let T=o.useCallback(e=>{let{tabbingDirection:t}=e,r=c().map(e=>{let r=e.ref.current,n=[r,...function(e){let t=[],r=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT,{acceptNode:e=>{let t="INPUT"===e.tagName&&"hidden"===e.type;return e.disabled||e.hidden||t?NodeFilter.FILTER_SKIP:e.tabIndex>=0?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_SKIP}});for(;r.nextNode();)t.push(r.currentNode);return t}(r)];return"forwards"===t?n:n.reverse()});return("forwards"===t?r.reverse():r).flat()},[c]);return o.useEffect(()=>{let e=w.current;if(e){let t=t=>{let r=t.altKey||t.ctrlKey||t.metaKey;if("Tab"===t.key&&!r){var n,o,s;let r=document.activeElement,a=t.shiftKey;if(t.target===e&&a){null===(n=v.current)||void 0===n||n.focus();return}let i=T({tabbingDirection:a?"backwards":"forwards"}),l=i.findIndex(e=>e===r);G(i.slice(l+1))?t.preventDefault():a?null===(o=v.current)||void 0===o||o.focus():null===(s=m.current)||void 0===s||s.focus()}};return e.addEventListener("keydown",t),()=>e.removeEventListener("keydown",t)}},[c,T]),(0,n.jsxs)(u.I0,{ref:f,role:"region","aria-label":a.replace("{hotkey}",y),tabIndex:-1,style:{pointerEvents:E?void 0:"none"},children:[E&&(0,n.jsx)(I,{ref:v,onFocusFromOutsideViewport:()=>{G(T({tabbingDirection:"forwards"}))}}),(0,n.jsx)(g.Slot,{scope:r,children:(0,n.jsx)(p.WV.ol,{tabIndex:-1,...l,ref:x})}),E&&(0,n.jsx)(I,{ref:m,onFocusFromOutsideViewport:()=>{G(T({tabbingDirection:"backwards"}))}})]})});_.displayName=P;var A="ToastFocusProxy",I=o.forwardRef((e,t)=>{let{__scopeToast:r,onFocusFromOutsideViewport:o,...s}=e,a=R(A,r);return(0,n.jsx)(x.T,{"aria-hidden":!0,tabIndex:0,...s,ref:t,style:{position:"fixed"},onFocus:e=>{var t;let r=e.relatedTarget;(null===(t=a.viewport)||void 0===t?void 0:t.contains(r))||o()}})});I.displayName=A;var M="Toast",L=o.forwardRef((e,t)=>{let{forceMount:r,open:o,defaultOpen:s,onOpenChange:i,...l}=e,[d=!0,u]=(0,m.T)({prop:o,defaultProp:s,onChange:i});return(0,n.jsx)(f.z,{present:r||d,children:(0,n.jsx)(O,{open:d,...l,ref:t,onClose:()=>u(!1),onPause:(0,v.W)(e.onPause),onResume:(0,v.W)(e.onResume),onSwipeStart:(0,a.M)(e.onSwipeStart,e=>{e.currentTarget.setAttribute("data-swipe","start")}),onSwipeMove:(0,a.M)(e.onSwipeMove,e=>{let{x:t,y:r}=e.detail.delta;e.currentTarget.setAttribute("data-swipe","move"),e.currentTarget.style.setProperty("--radix-toast-swipe-move-x","".concat(t,"px")),e.currentTarget.style.setProperty("--radix-toast-swipe-move-y","".concat(r,"px"))}),onSwipeCancel:(0,a.M)(e.onSwipeCancel,e=>{e.currentTarget.setAttribute("data-swipe","cancel"),e.currentTarget.style.removeProperty("--radix-toast-swipe-move-x"),e.currentTarget.style.removeProperty("--radix-toast-swipe-move-y"),e.currentTarget.style.removeProperty("--radix-toast-swipe-end-x"),e.currentTarget.style.removeProperty("--radix-toast-swipe-end-y")}),onSwipeEnd:(0,a.M)(e.onSwipeEnd,e=>{let{x:t,y:r}=e.detail.delta;e.currentTarget.setAttribute("data-swipe","end"),e.currentTarget.style.removeProperty("--radix-toast-swipe-move-x"),e.currentTarget.style.removeProperty("--radix-toast-swipe-move-y"),e.currentTarget.style.setProperty("--radix-toast-swipe-end-x","".concat(t,"px")),e.currentTarget.style.setProperty("--radix-toast-swipe-end-y","".concat(r,"px")),u(!1)})})})});L.displayName=M;var[k,F]=T(M,{onClose(){}}),O=o.forwardRef((e,t)=>{let{__scopeToast:r,type:l="foreground",duration:d,open:c,onClose:f,onEscapeKeyDown:m,onPause:w,onResume:x,onSwipeStart:y,onSwipeMove:h,onSwipeCancel:E,onSwipeEnd:T,...b}=e,N=R(M,r),[j,P]=o.useState(null),S=(0,i.e)(t,e=>P(e)),_=o.useRef(null),A=o.useRef(null),I=d||N.duration,L=o.useRef(0),F=o.useRef(I),O=o.useRef(0),{onToastAdd:K,onToastRemove:W}=N,U=(0,v.W)(()=>{var e;(null==j?void 0:j.contains(document.activeElement))&&(null===(e=N.viewport)||void 0===e||e.focus()),f()}),X=o.useCallback(e=>{e&&e!==1/0&&(window.clearTimeout(O.current),L.current=new Date().getTime(),O.current=window.setTimeout(U,e))},[U]);o.useEffect(()=>{let e=N.viewport;if(e){let t=()=>{X(F.current),null==x||x()},r=()=>{let e=new Date().getTime()-L.current;F.current=F.current-e,window.clearTimeout(O.current),null==w||w()};return e.addEventListener(C,r),e.addEventListener(D,t),()=>{e.removeEventListener(C,r),e.removeEventListener(D,t)}}},[N.viewport,I,w,x,X]),o.useEffect(()=>{c&&!N.isClosePausedRef.current&&X(I)},[c,I,N.isClosePausedRef,X]),o.useEffect(()=>(K(),()=>W()),[K,W]);let H=o.useMemo(()=>j?function e(t){let r=[];return Array.from(t.childNodes).forEach(t=>{if(t.nodeType===t.TEXT_NODE&&t.textContent&&r.push(t.textContent),t.nodeType===t.ELEMENT_NODE){let n=t.ariaHidden||t.hidden||"none"===t.style.display,o=""===t.dataset.radixToastAnnounceExclude;if(!n){if(o){let e=t.dataset.radixToastAnnounceAlt;e&&r.push(e)}else r.push(...e(t))}}}),r}(j):null,[j]);return N.viewport?(0,n.jsxs)(n.Fragment,{children:[H&&(0,n.jsx)(V,{__scopeToast:r,role:"status","aria-live":"foreground"===l?"assertive":"polite","aria-atomic":!0,children:H}),(0,n.jsx)(k,{scope:r,onClose:U,children:s.createPortal((0,n.jsx)(g.ItemSlot,{scope:r,children:(0,n.jsx)(u.fC,{asChild:!0,onEscapeKeyDown:(0,a.M)(m,()=>{N.isFocusedToastEscapeKeyDownRef.current||U(),N.isFocusedToastEscapeKeyDownRef.current=!1}),children:(0,n.jsx)(p.WV.li,{role:"status","aria-live":"off","aria-atomic":!0,tabIndex:0,"data-state":c?"open":"closed","data-swipe-direction":N.swipeDirection,...b,ref:S,style:{userSelect:"none",touchAction:"none",...e.style},onKeyDown:(0,a.M)(e.onKeyDown,e=>{"Escape"!==e.key||(null==m||m(e.nativeEvent),e.nativeEvent.defaultPrevented||(N.isFocusedToastEscapeKeyDownRef.current=!0,U()))}),onPointerDown:(0,a.M)(e.onPointerDown,e=>{0===e.button&&(_.current={x:e.clientX,y:e.clientY})}),onPointerMove:(0,a.M)(e.onPointerMove,e=>{if(!_.current)return;let t=e.clientX-_.current.x,r=e.clientY-_.current.y,n=!!A.current,o=["left","right"].includes(N.swipeDirection),s=["left","up"].includes(N.swipeDirection)?Math.min:Math.max,a=o?s(0,t):0,i=o?0:s(0,r),l="touch"===e.pointerType?10:2,d={x:a,y:i},u={originalEvent:e,delta:d};n?(A.current=d,Y("toast.swipeMove",h,u,{discrete:!1})):B(d,N.swipeDirection,l)?(A.current=d,Y("toast.swipeStart",y,u,{discrete:!1}),e.target.setPointerCapture(e.pointerId)):(Math.abs(t)>l||Math.abs(r)>l)&&(_.current=null)}),onPointerUp:(0,a.M)(e.onPointerUp,e=>{let t=A.current,r=e.target;if(r.hasPointerCapture(e.pointerId)&&r.releasePointerCapture(e.pointerId),A.current=null,_.current=null,t){let r=e.currentTarget,n={originalEvent:e,delta:t};B(t,N.swipeDirection,N.swipeThreshold)?Y("toast.swipeEnd",T,n,{discrete:!0}):Y("toast.swipeCancel",E,n,{discrete:!0}),r.addEventListener("click",e=>e.preventDefault(),{once:!0})}})})})}),N.viewport)})]}):null}),V=e=>{let{__scopeToast:t,children:r,...s}=e,a=R(M,t),[i,l]=o.useState(!1),[d,u]=o.useState(!1);return function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:()=>{},t=(0,v.W)(e);(0,w.b)(()=>{let e=0,r=0;return e=window.requestAnimationFrame(()=>r=window.requestAnimationFrame(t)),()=>{window.cancelAnimationFrame(e),window.cancelAnimationFrame(r)}},[t])}(()=>l(!0)),o.useEffect(()=>{let e=window.setTimeout(()=>u(!0),1e3);return()=>window.clearTimeout(e)},[]),d?null:(0,n.jsx)(c.h,{asChild:!0,children:(0,n.jsx)(x.T,{...s,children:i&&(0,n.jsxs)(n.Fragment,{children:[a.label," ",r]})})})},K=o.forwardRef((e,t)=>{let{__scopeToast:r,...o}=e;return(0,n.jsx)(p.WV.div,{...o,ref:t})});K.displayName="ToastTitle";var W=o.forwardRef((e,t)=>{let{__scopeToast:r,...o}=e;return(0,n.jsx)(p.WV.div,{...o,ref:t})});W.displayName="ToastDescription";var U="ToastAction",X=o.forwardRef((e,t)=>{let{altText:r,...o}=e;return r.trim()?(0,n.jsx)(z,{altText:r,asChild:!0,children:(0,n.jsx)(q,{...o,ref:t})}):(console.error("Invalid prop `altText` supplied to `".concat(U,"`. Expected non-empty `string`.")),null)});X.displayName=U;var H="ToastClose",q=o.forwardRef((e,t)=>{let{__scopeToast:r,...o}=e,s=F(H,r);return(0,n.jsx)(z,{asChild:!0,children:(0,n.jsx)(p.WV.button,{type:"button",...o,ref:t,onClick:(0,a.M)(e.onClick,s.onClose)})})});q.displayName=H;var z=o.forwardRef((e,t)=>{let{__scopeToast:r,altText:o,...s}=e;return(0,n.jsx)(p.WV.div,{"data-radix-toast-announce-exclude":"","data-radix-toast-announce-alt":o||void 0,...s,ref:t})});function Y(e,t,r,n){let{discrete:o}=n,s=r.originalEvent.currentTarget,a=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:r});t&&s.addEventListener(e,t,{once:!0}),o?(0,p.jH)(s,a):s.dispatchEvent(a)}var B=function(e,t){let r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,n=Math.abs(e.x),o=Math.abs(e.y),s=n>o;return"left"===t||"right"===t?s&&n>r:!s&&o>r};function G(e){let t=document.activeElement;return e.some(e=>e===t||(e.focus(),document.activeElement!==t))}var Z=r(535),J=r(2489),Q=r(4508);let $=o.forwardRef((e,t)=>{let{className:r,...o}=e;return(0,n.jsx)(_,{ref:t,className:(0,Q.cn)("fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",r),...o})});$.displayName=_.displayName;let ee=(0,Z.j)("group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",{variants:{variant:{default:"border bg-background text-foreground",destructive:"destructive group border-destructive bg-destructive text-destructive-foreground"}},defaultVariants:{variant:"default"}}),et=o.forwardRef((e,t)=>{let{className:r,variant:o,...s}=e;return(0,n.jsx)(L,{ref:t,className:(0,Q.cn)(ee({variant:o}),r),...s})});et.displayName=L.displayName,o.forwardRef((e,t)=>{let{className:r,...o}=e;return(0,n.jsx)(X,{ref:t,className:(0,Q.cn)("inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",r),...o})}).displayName=X.displayName;let er=o.forwardRef((e,t)=>{let{className:r,...o}=e;return(0,n.jsx)(q,{ref:t,className:(0,Q.cn)("absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",r),"toast-close":"",...o,children:(0,n.jsx)(J.Z,{className:"h-4 w-4"})})});er.displayName=q.displayName;let en=o.forwardRef((e,t)=>{let{className:r,...o}=e;return(0,n.jsx)(K,{ref:t,className:(0,Q.cn)("text-sm font-semibold",r),...o})});en.displayName=K.displayName;let eo=o.forwardRef((e,t)=>{let{className:r,...o}=e;return(0,n.jsx)(W,{ref:t,className:(0,Q.cn)("text-sm opacity-90",r),...o})});eo.displayName=W.displayName;var es=r(7992);function ea(){let{toasts:e}=(0,es.pm)();return(0,n.jsxs)(j,{children:[e.map(function(e){let{id:t,title:r,description:o,action:s,...a}=e;return(0,n.jsxs)(et,{...a,children:[(0,n.jsxs)("div",{className:"grid gap-1",children:[r&&(0,n.jsx)(en,{children:r}),o&&(0,n.jsx)(eo,{children:o})]}),s,(0,n.jsx)(er,{})]},t)}),(0,n.jsx)($,{})]})}},7992:function(e,t,r){"use strict";r.d(t,{pm:function(){return f}});var n=r(2265);let o=0,s=new Map,a=e=>{if(s.has(e))return;let t=setTimeout(()=>{s.delete(e),u({type:"REMOVE_TOAST",toastId:e})},1e6);s.set(e,t)},i=(e,t)=>{switch(t.type){case"ADD_TOAST":return{...e,toasts:[t.toast,...e.toasts].slice(0,1)};case"UPDATE_TOAST":return{...e,toasts:e.toasts.map(e=>e.id===t.toast.id?{...e,...t.toast}:e)};case"DISMISS_TOAST":{let{toastId:r}=t;return r?a(r):e.toasts.forEach(e=>{a(e.id)}),{...e,toasts:e.toasts.map(e=>e.id===r||void 0===r?{...e,open:!1}:e)}}case"REMOVE_TOAST":if(void 0===t.toastId)return{...e,toasts:[]};return{...e,toasts:e.toasts.filter(e=>e.id!==t.toastId)}}},l=[],d={toasts:[]};function u(e){d=i(d,e),l.forEach(e=>{e(d)})}function c(e){let{...t}=e,r=(o=(o+1)%Number.MAX_SAFE_INTEGER).toString(),n=()=>u({type:"DISMISS_TOAST",toastId:r});return u({type:"ADD_TOAST",toast:{...t,id:r,open:!0,onOpenChange:e=>{e||n()}}}),{id:r,dismiss:n,update:e=>u({type:"UPDATE_TOAST",toast:{...e,id:r}})}}function f(){let[e,t]=n.useState(d);return n.useEffect(()=>(l.push(t),()=>{let e=l.indexOf(t);e>-1&&l.splice(e,1)}),[e]),{...e,toast:c,dismiss:e=>u({type:"DISMISS_TOAST",toastId:e})}}},4508:function(e,t,r){"use strict";r.d(t,{cn:function(){return s}});var n=r(1994),o=r(3335);function s(){for(var e=arguments.length,t=Array(e),r=0;rsvg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",{variants:{variant:{default:"bg-background text-foreground",destructive:"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive"}},defaultVariants:{variant:"default"}}),m=a.forwardRef((e,t)=>{let{className:r,variant:a,...s}=e;return(0,n.jsx)("div",{ref:t,role:"alert",className:(0,c.cn)(f({variant:a}),r),...s})});m.displayName="Alert",a.forwardRef((e,t)=>{let{className:r,...a}=e;return(0,n.jsx)("h5",{ref:t,className:(0,c.cn)("mb-1 font-medium leading-none tracking-tight",r),...a})}).displayName="AlertTitle";let g=a.forwardRef((e,t)=>{let{className:r,...a}=e;return(0,n.jsx)("div",{ref:t,className:(0,c.cn)("text-sm [&_p]:leading-relaxed",r),...a})});g.displayName="AlertDescription";let p=(0,r(9205).Z)("CircleAlert",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12",key:"1pkeuh"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16",key:"4dfq90"}]]);function v(){(0,s.useRouter)();let[e,t]=(0,a.useState)(""),[r,u]=(0,a.useState)(""),[c,f]=(0,a.useState)(!1),[v,x]=(0,a.useState)(""),b=async t=>{t.preventDefault(),f(!0),x("");try{let t=await fetch("/api/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:e,password:r})}),n=await t.json();t.ok&&0===n.code?(localStorage.setItem("terminal_auth_token",n.data.token),document.cookie="terminal_auth_token=".concat(n.data.token,"; path=/; max-age=86400; SameSite=Strict"),x(""),setTimeout(()=>{window.location.href="/"},500)):x(n.message||"Login failed, please check your username and password")}catch(e){x("Login request failed, please try again later")}finally{f(!1)}};return(0,n.jsx)("div",{className:"flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-900",children:(0,n.jsxs)(i.Zb,{className:"w-full max-w-md",children:[(0,n.jsx)(i.Ol,{className:"space-y-1"}),(0,n.jsxs)("form",{onSubmit:b,children:[(0,n.jsxs)(i.aY,{className:"space-y-4",children:[v&&(0,n.jsxs)(m,{variant:"destructive",children:[(0,n.jsx)(p,{className:"h-4 w-4"}),(0,n.jsx)(g,{children:v})]}),(0,n.jsxs)("div",{className:"space-y-2",children:[(0,n.jsx)(d._,{htmlFor:"username",children:"Username"}),(0,n.jsx)(l.I,{id:"username",placeholder:"",value:e,onChange:e=>t(e.target.value),required:!0})]}),(0,n.jsxs)("div",{className:"space-y-2",children:[(0,n.jsx)(d._,{htmlFor:"password",children:"Password"}),(0,n.jsx)(l.I,{id:"password",type:"password",placeholder:"",value:r,onChange:e=>u(e.target.value),required:!0})]})]}),(0,n.jsx)(i.eW,{children:(0,n.jsx)(o.z,{type:"submit",className:"w-full",disabled:c,children:c?"Logging in...":"Login"})})]})]})})}},2869:function(e,t,r){"use strict";r.d(t,{z:function(){return d}});var n=r(7437),a=r(2265),s=r(7053),i=r(535),o=r(4508);let l=(0,i.j)("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",{variants:{variant:{default:"bg-primary text-primary-foreground hover:bg-primary/90",destructive:"bg-destructive text-destructive-foreground hover:bg-destructive/90",outline:"border border-input bg-background hover:bg-accent hover:text-accent-foreground",secondary:"bg-secondary text-secondary-foreground hover:bg-secondary/80",ghost:"hover:bg-accent hover:text-accent-foreground",link:"text-primary underline-offset-4 hover:underline"},size:{default:"h-10 px-4 py-2",sm:"h-9 rounded-md px-3",lg:"h-11 rounded-md px-8",icon:"h-10 w-10"}},defaultVariants:{variant:"default",size:"default"}}),d=a.forwardRef((e,t)=>{let{className:r,variant:a,size:i,asChild:d=!1,...u}=e,c=d?s.g7:"button";return(0,n.jsx)(c,{className:(0,o.cn)(l({variant:a,size:i,className:r})),ref:t,...u})});d.displayName="Button"},6070:function(e,t,r){"use strict";r.d(t,{Ol:function(){return o},Zb:function(){return i},aY:function(){return l},eW:function(){return d}});var n=r(7437),a=r(2265),s=r(4508);let i=a.forwardRef((e,t)=>{let{className:r,...a}=e;return(0,n.jsx)("div",{ref:t,className:(0,s.cn)("rounded-lg border bg-card text-card-foreground shadow-sm",r),...a})});i.displayName="Card";let o=a.forwardRef((e,t)=>{let{className:r,...a}=e;return(0,n.jsx)("div",{ref:t,className:(0,s.cn)("flex flex-col space-y-1.5 p-6",r),...a})});o.displayName="CardHeader",a.forwardRef((e,t)=>{let{className:r,...a}=e;return(0,n.jsx)("div",{ref:t,className:(0,s.cn)("text-2xl font-semibold leading-none tracking-tight",r),...a})}).displayName="CardTitle",a.forwardRef((e,t)=>{let{className:r,...a}=e;return(0,n.jsx)("div",{ref:t,className:(0,s.cn)("text-sm text-muted-foreground",r),...a})}).displayName="CardDescription";let l=a.forwardRef((e,t)=>{let{className:r,...a}=e;return(0,n.jsx)("div",{ref:t,className:(0,s.cn)("p-6 pt-0",r),...a})});l.displayName="CardContent";let d=a.forwardRef((e,t)=>{let{className:r,...a}=e;return(0,n.jsx)("div",{ref:t,className:(0,s.cn)("flex items-center p-6 pt-0",r),...a})});d.displayName="CardFooter"},5186:function(e,t,r){"use strict";r.d(t,{I:function(){return i}});var n=r(7437),a=r(2265),s=r(4508);let i=a.forwardRef((e,t)=>{let{className:r,type:a,...i}=e;return(0,n.jsx)("input",{type:a,className:(0,s.cn)("flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",r),ref:t,...i})});i.displayName="Input"},6815:function(e,t,r){"use strict";r.d(t,{_:function(){return d}});var n=r(7437),a=r(2265),s=r(6394),i=r(535),o=r(4508);let l=(0,i.j)("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"),d=a.forwardRef((e,t)=>{let{className:r,...a}=e;return(0,n.jsx)(s.f,{ref:t,className:(0,o.cn)(l(),r),...a})});d.displayName=s.f.displayName},4508:function(e,t,r){"use strict";r.d(t,{cn:function(){return s}});var n=r(1994),a=r(3335);function s(){for(var e=arguments.length,t=Array(e),r=0;r(0,s.jsx)(a.WV.label,{...e,ref:t,onMouseDown:t=>{var r;t.target.closest("button, input, select, textarea")||(null===(r=e.onMouseDown)||void 0===r||r.call(e,t),!t.defaultPrevented&&t.detail>1&&t.preventDefault())}}));i.displayName="Label";var o=i}},function(e){e.O(0,[253,971,117,744],function(){return e(e.s=9733)}),_N_E=e.O()}]); -------------------------------------------------------------------------------- /src/web/out/_next/static/chunks/main-app-922bba6b3ffca5f3.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[744],{3065:function(e,n,t){Promise.resolve().then(t.t.bind(t,2846,23)),Promise.resolve().then(t.t.bind(t,9107,23)),Promise.resolve().then(t.t.bind(t,1060,23)),Promise.resolve().then(t.t.bind(t,4707,23)),Promise.resolve().then(t.t.bind(t,80,23)),Promise.resolve().then(t.t.bind(t,6423,23))}},function(e){var n=function(n){return e(e.s=n)};e.O(0,[971,117],function(){return n(4278),n(3065)}),_N_E=e.O()}]); -------------------------------------------------------------------------------- /src/web/out/_next/static/chunks/pages/_app-72b849fbd24ac258.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[888],{1597:function(n,_,u){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_app",function(){return u(8141)}])}},function(n){var _=function(_){return n(n.s=_)};n.O(0,[774,179],function(){return _(1597),_(7253)}),_N_E=n.O()}]); -------------------------------------------------------------------------------- /src/web/out/_next/static/chunks/pages/_error-7ba65e1336b92748.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[820],{1981:function(n,_,u){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_error",function(){return u(8529)}])}},function(n){n.O(0,[888,774,179],function(){return n(n.s=1981)}),_N_E=n.O()}]); -------------------------------------------------------------------------------- /src/web/out/_next/static/chunks/webpack-843b083001d99fcc.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";var e,t,n,r,o,u,i,f,c,a,l,d,s={},p={};function b(e){var t=p[e];if(void 0!==t)return t.exports;var n=p[e]={exports:{}},r=!0;try{s[e](n,n.exports,b),r=!1}finally{r&&delete p[e]}return n.exports}b.m=s,e=[],b.O=function(t,n,r,o){if(n){o=o||0;for(var u=e.length;u>0&&e[u-1][2]>o;u--)e[u]=e[u-1];e[u]=[n,r,o];return}for(var i=1/0,u=0;u=o&&Object.keys(b.O).every(function(e){return b.O[e](n[c])})?n.splice(c--,1):(f=!1,oGhostEye

    Loading...

      -------------------------------------------------------------------------------- /src/web/out/index.txt: -------------------------------------------------------------------------------- 1 | 2:I[9107,[],"ClientPageRoot"] 2 | 3:I[8609,["253","static/chunks/253-95c9bbffcbc06f61.js","747","static/chunks/747-b7871a7d8b45f8b8.js","130","static/chunks/130-657e507e7c0a9bfa.js","931","static/chunks/app/page-b5682bda19311bce.js"],"default",1] 3 | 4:I[4707,[],""] 4 | 5:I[6423,[],""] 5 | 6:I[4694,["253","static/chunks/253-95c9bbffcbc06f61.js","747","static/chunks/747-b7871a7d8b45f8b8.js","185","static/chunks/app/layout-2494ccb28bcae9b7.js"],"Toaster"] 6 | 0:["TFz4HhDxhTU5FA-Rc0bx2",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/a2d275681cc35a24.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_d65c78","children":[["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}],["$","$L6",null,{}]]}]}]],null],null],["$L7",null]]]] 7 | 7:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"GhostEye"}],["$","meta","3",{"name":"description","content":"Remote Control Tool"}]] 8 | 1:null 9 | -------------------------------------------------------------------------------- /src/web/out/login.html: -------------------------------------------------------------------------------- 1 | GhostEye
        -------------------------------------------------------------------------------- /src/web/out/login.txt: -------------------------------------------------------------------------------- 1 | 2:I[9107,[],"ClientPageRoot"] 2 | 3:I[988,["253","static/chunks/253-95c9bbffcbc06f61.js","626","static/chunks/app/login/page-e8df8927cd5da1d8.js"],"default",1] 3 | 4:I[4707,[],""] 4 | 5:I[6423,[],""] 5 | 6:I[4694,["253","static/chunks/253-95c9bbffcbc06f61.js","747","static/chunks/747-b7871a7d8b45f8b8.js","185","static/chunks/app/layout-2494ccb28bcae9b7.js"],"Toaster"] 6 | 0:["TFz4HhDxhTU5FA-Rc0bx2",[[["",{"children":["login",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["login",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","login","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/a2d275681cc35a24.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_d65c78","children":[["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}],["$","$L6",null,{}]]}]}]],null],null],["$L7",null]]]] 7 | 7:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"GhostEye"}],["$","meta","3",{"name":"description","content":"Remote Control Tool"}]] 8 | 1:null 9 | -------------------------------------------------------------------------------- /src/web/out/placeholder-logo.png: -------------------------------------------------------------------------------- 1 | �PNG 2 |  3 | IHDR�M��0PLTEZ? tRNS� �@��`P0p���w �IDATx��ؽJ3Q�7'��%�|?� ���E�l�7���(X�D������w`����[�*t����D���mD�}��4; ;�DDDDDDDDDDDD_�_İ��!�y�`�_�:�� ;Ļ�'|� ��;.I"����3*5����J�1�� �T��FI�� ��=��3܃�2~�b���0��U9\��]�4�#w0��Gt\&1 �?21,���o!e�m��ĻR�����5�� ؽAJ�9��R)�5�0.FFASaǃ�T�#|�K���I�������1� 4 | M������N"��$����G�V�T� ��T^^��A�$S��h(�������G]co"J׸^^�'�=���%� �W�6Ы�W��w�a�߇*�^^�YG�c���`'F����������������^5_�,�S�%IEND�B`� -------------------------------------------------------------------------------- /src/web/out/placeholder-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web/out/placeholder-user.jpg: -------------------------------------------------------------------------------- 1 | ����JFIF��C 2 |   3 |  4 | $ &%# #"(-90(*6+"#2D26;=@@@&0FKE>J9?@=��C  =)#)==================================================���������� ـ|�r4�-�̈"x�'�0�Í��8�H�N�q�����Q�������V�`=�($q"_�� 5 | �S8�P��0VFbP��! 6 | Io40��[?p#�|� @!.E�3��4pBq �Z s���C  AQR�!1@Ua��02Tq���56cps�� "#$PS�����?��R���,�� ��� 7 | �n�k��n8rZ�����9Vv��V��ms$9zWʏh�-+@Z�2�PGE������EY9��i�Ͻ�S ��O��Ȕ��_I��W髵�}�����B�ՎT��>%r �[e/,W�D}��D�>b�e>�v�Z�p&�*VS��V�sV�c�:��~K�������C��:��k�'An|ʶ�}\��C� �f����a�;�h��J����q!i=���"�q�NF�IZ�`�wĝ5hAj� 8 | �RXl䎉�lk���@I�%l��Ն���FDY-����Eq�i����O�I�_�2b�lNj�Yu��k���AO����٣��ܭ�n��cam�jN�j���VL�}� ;��oކ6��շs��,���ք���l�i����l{I�O��(!%J $ ���n�-@G����n��ܮi!�괁G�:�^��n�g3l�F%���9]�Pq��)�:��� @�*ɍmׅ�VLY'�s+�z ���V�m�J9��S�_���#��;�����NJ!5�#�q\�M@��@�]yz�����A;e�k��@�s�^���G����\�5F��(��S��Ly���c�i8�����o�8T��i�N��7D����t-�p�3`r�q r�;|�.��bTG��[i H��͚-� 9 | ��Oj�H����M�ؒFE�{�3X�n���e� �R3/�~����� 10 | ��a����!�j&@^r�����Y�������l�Z? �7땵��)ki�w��\.�u�����X��\.�u�����X��\.�u�����X��\.�u��p�M����o(N��3�Vg�����Z�s��%�\�]q}d�k\_Y5���MG�����Q��q}d�k\_Y5���MG�����Q��q}d�kV|5���8���//�������?�����?�� -------------------------------------------------------------------------------- /src/web/out/placeholder.jpg: -------------------------------------------------------------------------------- 1 | ����JFIFHH���ExifMM*JR(�iZHH�����8Photoshop 3.08BIM8BIM%��ُ�� ���B~���� 2 | ���s!1"AQ2aq#� �B�R3�$b0�r�C�4��S@%c5�s�PD���&T6d�t�`҄�p�'E7e�Uu��Å��Fv��GVf� 3 | ()*89:HIJWXYZghijwxyz����������������������������������������������������������� 4 | ����! 1A0"2Q@3#aBqR4�P$��C�b5S��%`�D�r��c6p&ET�'�� 5 | ()*789:FGHIJUVWXYZdefghijstuvwxyz�����������������������������������������������������������������������������C  6 |  7 | 8 | ")$+*($''-2@7-0=0''8L9=CEHIH+6OUNFT@GHE��C !!E.'.EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE�� �k����?��?��?��3 !1AQaq��������� 0@P`p���������?!��� ��3 !1AQa q𑁡�����0@P`p���������?���?���?��� -------------------------------------------------------------------------------- /src/web/out/placeholder.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------