Refactor install command generation and update response structure
Some checks failed
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Has been cancelled
Build Multi-Platform Binaries / build-frontend (push) Has been cancelled

This commit is contained in:
2026-03-19 20:49:23 +08:00
parent 8901581d0c
commit 6558d1acdb
4 changed files with 87 additions and 48 deletions

View File

@@ -3,7 +3,6 @@ package handler
import ( import (
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"fmt"
"net/http" "net/http"
"time" "time"
@@ -11,41 +10,38 @@ import (
"github.com/gotunnel/internal/server/db" "github.com/gotunnel/internal/server/db"
) )
// InstallHandler 安装处理器
type InstallHandler struct { type InstallHandler struct {
app AppInterface app AppInterface
} }
// NewInstallHandler 创建安装处理器
func NewInstallHandler(app AppInterface) *InstallHandler { func NewInstallHandler(app AppInterface) *InstallHandler {
return &InstallHandler{app: app} return &InstallHandler{app: app}
} }
// InstallCommandResponse 安装命令响应
type InstallCommandResponse struct { type InstallCommandResponse struct {
Token string `json:"token"` Token string `json:"token"`
Commands map[string]string `json:"commands"` ExpiresAt int64 `json:"expires_at"`
ExpiresAt int64 `json:"expires_at"` TunnelPort int `json:"tunnel_port"`
ServerAddr string `json:"server_addr"`
} }
// GenerateInstallCommand 生成安装命令 // GenerateInstallCommand creates a one-time install token and returns
// @Summary 生成客户端安装命令 // the tunnel port so the frontend can build a host-aware command.
//
// @Summary Generate install command payload
// @Tags install // @Tags install
// @Produce json // @Produce json
// @Success 200 {object} InstallCommandResponse // @Success 200 {object} InstallCommandResponse
// @Router /api/install/generate [post] // @Router /api/install/generate [post]
func (h *InstallHandler) GenerateInstallCommand(c *gin.Context) { func (h *InstallHandler) GenerateInstallCommand(c *gin.Context) {
// 生成随机token
tokenBytes := make([]byte, 32) tokenBytes := make([]byte, 32)
if _, err := rand.Read(tokenBytes); err != nil { if _, err := rand.Read(tokenBytes); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成token失败"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"})
return return
} }
token := hex.EncodeToString(tokenBytes)
// 保存到数据库 token := hex.EncodeToString(tokenBytes)
now := time.Now().Unix() now := time.Now().Unix()
installToken := &db.InstallToken{ installToken := &db.InstallToken{
Token: token, Token: token,
CreatedAt: now, CreatedAt: now,
@@ -54,36 +50,18 @@ func (h *InstallHandler) GenerateInstallCommand(c *gin.Context) {
store, ok := h.app.GetClientStore().(db.InstallTokenStore) store, ok := h.app.GetClientStore().(db.InstallTokenStore)
if !ok { if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "存储不支持安装token"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "install token store is not supported"})
return return
} }
if err := store.CreateInstallToken(installToken); err != nil { if err := store.CreateInstallToken(installToken); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存token失败"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to persist token"})
return return
} }
// 获取服务器地址
serverAddr := fmt.Sprintf("%s:%d", h.app.GetConfig().Server.BindAddr, h.app.GetServer().GetBindPort())
if h.app.GetConfig().Server.BindAddr == "" || h.app.GetConfig().Server.BindAddr == "0.0.0.0" {
serverAddr = fmt.Sprintf("your-server-ip:%d", h.app.GetServer().GetBindPort())
}
// 生成安装命令
expiresAt := now + 3600 // 1小时过期
commands := map[string]string{
"linux": fmt.Sprintf("curl -fsSL https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.sh | bash -s -- -s %s -t %s",
serverAddr, token),
"macos": fmt.Sprintf("curl -fsSL https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.sh | bash -s -- -s %s -t %s",
serverAddr, token),
"windows": fmt.Sprintf("powershell -c \"irm https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.ps1 | iex; Install-GoTunnel -Server '%s' -Token '%s'\"",
serverAddr, token),
}
c.JSON(http.StatusOK, InstallCommandResponse{ c.JSON(http.StatusOK, InstallCommandResponse{
Token: token, Token: token,
Commands: commands, ExpiresAt: now + 3600,
ExpiresAt: expiresAt, TunnelPort: h.app.GetServer().GetBindPort(),
ServerAddr: serverAddr,
}) })
} }

View File

@@ -189,7 +189,7 @@ onUnmounted(() => {
<div class="app-main"> <div class="app-main">
<header class="app-topbar glass-card"> <header class="app-topbar glass-card">
<div> <div class="topbar-intro">
<span class="topbar-label">Workspace</span> <span class="topbar-label">Workspace</span>
<h1>{{ navItems.find((item) => item.key === activeNav)?.label || '控制台' }}</h1> <h1>{{ navItems.find((item) => item.key === activeNav)?.label || '控制台' }}</h1>
</div> </div>
@@ -375,6 +375,34 @@ onUnmounted(() => {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 20px; gap: 20px;
position: relative;
overflow: visible;
z-index: 30;
padding: 18px 22px;
border-radius: 22px;
background:
radial-gradient(circle at top right, var(--color-accent-glow), transparent 38%),
linear-gradient(135deg, var(--glass-bg) 0%, var(--glass-bg-light) 100%);
border-color: rgba(255, 255, 255, 0.1);
}
.app-topbar::after {
content: '';
position: absolute;
inset: auto 18px -18px auto;
width: 120px;
height: 120px;
border-radius: 999px;
background: var(--color-accent-glow);
opacity: 0.18;
filter: blur(28px);
pointer-events: none;
}
.topbar-intro {
position: relative;
z-index: 1;
min-width: 0;
} }
.app-topbar h1 { .app-topbar h1 {
@@ -387,6 +415,9 @@ onUnmounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
position: relative;
z-index: 2;
flex-shrink: 0;
} }
.topbar-icon-btn, .topbar-icon-btn,
@@ -401,6 +432,15 @@ onUnmounted(() => {
background: var(--glass-bg-light); background: var(--glass-bg-light);
color: var(--color-text-primary); color: var(--color-text-primary);
cursor: pointer; cursor: pointer;
transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
}
.topbar-icon-btn:hover,
.profile-button:hover {
transform: translateY(-1px);
border-color: rgba(59, 130, 246, 0.28);
background: rgba(255, 255, 255, 0.08);
box-shadow: var(--shadow-sm);
} }
.topbar-icon-btn svg, .topbar-icon-btn svg,
@@ -412,6 +452,7 @@ onUnmounted(() => {
.menu-wrap { .menu-wrap {
position: relative; position: relative;
z-index: 4;
} }
.floating-menu { .floating-menu {
@@ -424,7 +465,9 @@ onUnmounted(() => {
background: var(--glass-bg); background: var(--glass-bg);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
z-index: 10; backdrop-filter: var(--glass-blur-light);
-webkit-backdrop-filter: var(--glass-blur-light);
z-index: 50;
} }
.floating-menu--right { .floating-menu--right {
@@ -514,6 +557,7 @@ onUnmounted(() => {
.app-topbar { .app-topbar {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
overflow: visible;
} }
.topbar-actions { .topbar-actions {

View File

@@ -67,11 +67,6 @@ export interface LogStreamOptions {
// 安装命令响应 // 安装命令响应
export interface InstallCommandResponse { export interface InstallCommandResponse {
token: string token: string
commands: {
linux: string
macos: string
windows: string
}
expires_at: number expires_at: number
server_addr: string tunnel_port: number
} }

View File

@@ -17,6 +17,27 @@ const showInstallModal = ref(false)
const installData = ref<InstallCommandResponse | null>(null) const installData = ref<InstallCommandResponse | null>(null)
const generatingInstall = ref(false) const generatingInstall = ref(false)
const search = ref('') const search = ref('')
const installScriptUrl = 'https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.sh'
const installPs1Url = 'https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.ps1'
const quoteShellArg = (value: string) => `'${value.replace(/'/g, `'\"'\"'`)}'`
const resolveTunnelHost = () => window.location.hostname || 'localhost'
const formatServerAddr = (host: string, port: number) => {
const normalizedHost = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host
return `${normalizedHost}:${port}`
}
const buildInstallCommands = (data: InstallCommandResponse) => {
const serverAddr = formatServerAddr(resolveTunnelHost(), data.tunnel_port)
return {
linux: `bash <(curl -fsSL ${installScriptUrl}) -s ${quoteShellArg(serverAddr)} -t ${quoteShellArg(data.token)}`,
macos: `bash <(curl -fsSL ${installScriptUrl}) -s ${quoteShellArg(serverAddr)} -t ${quoteShellArg(data.token)}`,
windows: `powershell -c \"irm ${installPs1Url} | iex; Install-GoTunnel -Server '${serverAddr}' -Token '${data.token}'\"`,
}
}
const loadClients = async () => { const loadClients = async () => {
loading.value = true loading.value = true
@@ -67,6 +88,7 @@ const filteredClients = computed(() => {
const onlineClients = computed(() => clients.value.filter((client) => client.online).length) const onlineClients = computed(() => clients.value.filter((client) => client.online).length)
const offlineClients = computed(() => Math.max(clients.value.length - onlineClients.value, 0)) const offlineClients = computed(() => Math.max(clients.value.length - onlineClients.value, 0))
const installCommands = computed(() => (installData.value ? buildInstallCommands(installData.value) : null))
onMounted(loadClients) onMounted(loadClients)
</script> </script>
@@ -126,11 +148,11 @@ onMounted(loadClients)
</SectionCard> </SectionCard>
<GlassModal :show="showInstallModal" title="安装命令" width="760px" @close="showInstallModal = false"> <GlassModal :show="showInstallModal" title="安装命令" width="760px" @close="showInstallModal = false">
<div v-if="installData" class="install-grid"> <div v-if="installCommands" class="install-grid">
<article v-for="item in [ <article v-for="item in [
{ label: 'Linux', value: installData.commands.linux }, { label: 'Linux', value: installCommands.linux },
{ label: 'macOS', value: installData.commands.macos }, { label: 'macOS', value: installCommands.macos },
{ label: 'Windows', value: installData.commands.windows }, { label: 'Windows', value: installCommands.windows },
]" :key="item.label" class="install-card"> ]" :key="item.label" class="install-card">
<header> <header>
<strong>{{ item.label }}</strong> <strong>{{ item.label }}</strong>