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
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:
@@ -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"`
|
||||||
ServerAddr string `json:"server_addr"`
|
TunnelPort int `json:"tunnel_port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user