├── .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 | 
6 | 
7 | 
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 | 一键生成base64反弹命令,满足各种环境需求 |
45 |
46 |
47 | ⏱️ 持久会话 |
48 | 后台保留shell会话,即使退出登录或重启浏览器也能立即恢复终端状态 |
49 |
50 |
51 | 📶 无惧网络波动 |
52 | 专为不稳定网络环境设计,确保shell连接稳定可靠 |
53 |
54 |
55 | 📚 命令模板库 |
56 | 保存和管理常用命令,如信息收集、提权、下载工具等,一键调用无需重复输入 |
57 |
58 |
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 | 
151 |
152 | ### 🚀 快速调用
153 | - 在终端会话中只需点击已保存的命令即可自动填充到输入区
154 | - 可以先修改再执行,适应不同环境需求
155 | - 点击base64会自动将输入框的命令转换为`echo ""|base64 -d |bash`
156 | 
157 |
158 | ### 🔄 反弹Shell命令示例
159 | 
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.GhostEye404
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
--------------------------------------------------------------------------------
/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�� 0PLTE Z? 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��0 VFbP��!
6 | Io40 ��[?p #�|�@ !.E� 3��4p Bq �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 | ���� JFIF H H �� �Exif MM * J R( �i Z H H � � � �� 8Photoshop 3.0 8BIM 8BIM% ��ُ �� ���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 |
--------------------------------------------------------------------------------