feat: 全界面中文汉化 + 安装命令改为 curl 脚本
This commit is contained in:
parent
c6a57b5551
commit
28a6f4bc45
@ -39,14 +39,14 @@ interface Zone {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const STEP_LABELS: Record<string, string> = {
|
const STEP_LABELS: Record<string, string> = {
|
||||||
find_zone: "Lookup DNS Zone",
|
find_zone: "查找 DNS Zone",
|
||||||
create_tunnel: "Create Tunnel",
|
create_tunnel: "创建隆道",
|
||||||
configure_tunnel: "Configure Ingress",
|
configure_tunnel: "配置 Ingress",
|
||||||
create_dns: "Create DNS Record",
|
create_dns: "创建 DNS 记录",
|
||||||
create_access_app: "Create Access App",
|
create_access_app: "创建访问应用",
|
||||||
setup_ca: "Setup Short-lived Cert",
|
setup_ca: "配置短期证书",
|
||||||
get_token: "Get Tunnel Token",
|
get_token: "获取隆道 Token",
|
||||||
browser_rendering: "Browser Rendering",
|
browser_rendering: "浏览器渲染提醒",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CreatePage() {
|
export default function CreatePage() {
|
||||||
@ -143,24 +143,24 @@ export default function CreatePage() {
|
|||||||
<Rocket className="w-5 h-5 text-emerald-500" />
|
<Rocket className="w-5 h-5 text-emerald-500" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-[var(--text)]">Deploy SSH Host</h1>
|
<h1 className="text-2xl font-bold text-[var(--text)]">部署 SSH 主机</h1>
|
||||||
<p className="text-sm text-[var(--text-muted)]">
|
<p className="text-sm text-[var(--text-muted)]">
|
||||||
One form → Tunnel + DNS + Access App + Install Command
|
一键完成 → 隆道 + DNS + 访问应用 + 安装命令
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Form */}
|
{/* Form */}
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
<Field icon={<Server className="w-4 h-4" />} label="Server Name"
|
<Field icon={<Server className="w-4 h-4" />} label="服务器名称"
|
||||||
hint="A friendly label for this server, e.g. prod-web-1">
|
hint="服务器昵称,如 prod-web-1">
|
||||||
<input type="text" value={form.serverName} onChange={set("serverName")}
|
<input type="text" value={form.serverName} onChange={set("serverName")}
|
||||||
placeholder="prod-web-1" className="form-input" required />
|
placeholder="prod-web-1" className="form-input" required />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
{/* SSH Hostname — zone dropdown + subdomain */}
|
{/* SSH Hostname — zone dropdown + subdomain */}
|
||||||
<Field icon={<Globe className="w-4 h-4" />} label="SSH Hostname"
|
<Field icon={<Globe className="w-4 h-4" />} label="SSH 访问域名"
|
||||||
hint={hostname ? `Will create: ${hostname}` : "Select a zone, then enter a subdomain"}>
|
hint={hostname ? `将创建:${hostname}` : "选择 Zone 再填写子域名前缀"}>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<input
|
<input
|
||||||
@ -177,11 +177,11 @@ export default function CreatePage() {
|
|||||||
{zonesLoading ? (
|
{zonesLoading ? (
|
||||||
<div className="form-input flex items-center gap-2 text-[var(--text-faint)]">
|
<div className="form-input flex items-center gap-2 text-[var(--text-faint)]">
|
||||||
<Loader2 className="w-3.5 h-3.5 animate-spin shrink-0" />
|
<Loader2 className="w-3.5 h-3.5 animate-spin shrink-0" />
|
||||||
<span className="text-sm">Loading zones…</span>
|
<span className="text-sm">加载 Zone 中…</span>
|
||||||
</div>
|
</div>
|
||||||
) : zonesError ? (
|
) : zonesError ? (
|
||||||
<div className="form-input flex items-center gap-2">
|
<div className="form-input flex items-center gap-2">
|
||||||
<span className="text-sm text-red-500 truncate flex-1">{zonesError}</span>
|
<span className="text-sm text-red-500 truncate flex-1">加载失败: {zonesError}</span>
|
||||||
<button type="button" onClick={fetchZones} className="shrink-0 text-[var(--text-muted)] hover:text-[var(--text)]">
|
<button type="button" onClick={fetchZones} className="shrink-0 text-[var(--text-muted)] hover:text-[var(--text)]">
|
||||||
<RefreshCw className="w-3.5 h-3.5" />
|
<RefreshCw className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@ -194,7 +194,7 @@ export default function CreatePage() {
|
|||||||
className="form-input appearance-none pr-8"
|
className="form-input appearance-none pr-8"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">Select zone…</option>
|
<option value="">选择 Zone…</option>
|
||||||
{zones.map((z) => (
|
{zones.map((z) => (
|
||||||
<option key={z.id} value={z.id}>{z.name}</option>
|
<option key={z.id} value={z.id}>{z.name}</option>
|
||||||
))}
|
))}
|
||||||
@ -206,20 +206,20 @@ export default function CreatePage() {
|
|||||||
</div>
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field icon={<Hash className="w-4 h-4" />} label="SSH Port"
|
<Field icon={<Hash className="w-4 h-4" />} label="SSH 端口"
|
||||||
hint="Server-side SSH port (usually 22)">
|
hint="服务器 SSH 端口(通常为 22)">
|
||||||
<input type="number" value={form.sshPort} onChange={set("sshPort")}
|
<input type="number" value={form.sshPort} onChange={set("sshPort")}
|
||||||
placeholder="22" className="form-input w-28" />
|
placeholder="22" className="form-input w-28" />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field icon={<Mail className="w-4 h-4" />} label="SSO Email"
|
<Field icon={<Mail className="w-4 h-4" />} label="SSO 邮筱"
|
||||||
hint="Your Cloudflare SSO email — its prefix becomes the Linux user">
|
hint="Cloudflare SSO 邮筱,前缀将作为 Linux 用户名">
|
||||||
<input type="email" value={form.ssoEmail} onChange={set("ssoEmail")}
|
<input type="email" value={form.ssoEmail} onChange={set("ssoEmail")}
|
||||||
placeholder="admin@example.com" className="form-input" required />
|
placeholder="admin@example.com" className="form-input" required />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field icon={<Mail className="w-4 h-4" />} label="Allowed Emails (optional)"
|
<Field icon={<Mail className="w-4 h-4" />} label="允许访问的邮筱(可选)"
|
||||||
hint="Comma-separated emails for the Access Policy. Leave empty = any authenticated user">
|
hint="多个邮筱用逗号分隔,留空表示所有已认证用户均可访问">
|
||||||
<textarea value={form.allowedEmails} onChange={set("allowedEmails")}
|
<textarea value={form.allowedEmails} onChange={set("allowedEmails")}
|
||||||
placeholder="alice@example.com, bob@example.com"
|
placeholder="alice@example.com, bob@example.com"
|
||||||
rows={2} className="form-input resize-none" />
|
rows={2} className="form-input resize-none" />
|
||||||
@ -231,9 +231,9 @@ export default function CreatePage() {
|
|||||||
className="w-full flex items-center justify-center gap-2 rounded-xl bg-emerald-600 hover:bg-emerald-500 disabled:opacity-40 disabled:cursor-not-allowed text-white font-semibold py-3 px-4 transition"
|
className="w-full flex items-center justify-center gap-2 rounded-xl bg-emerald-600 hover:bg-emerald-500 disabled:opacity-40 disabled:cursor-not-allowed text-white font-semibold py-3 px-4 transition"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<><Loader2 className="w-4 h-4 animate-spin" />Deploying…</>
|
<><Loader2 className="w-4 h-4 animate-spin" />部署中…</>
|
||||||
) : (
|
) : (
|
||||||
<><Rocket className="w-4 h-4" />Deploy Now</>
|
<><Rocket className="w-4 h-4" />立即部署</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@ -243,7 +243,7 @@ export default function CreatePage() {
|
|||||||
<div className="mt-6 rounded-xl border border-red-500/20 bg-red-500/5 p-4 flex items-start gap-3">
|
<div className="mt-6 rounded-xl border border-red-500/20 bg-red-500/5 p-4 flex items-start gap-3">
|
||||||
<XCircle className="w-5 h-5 text-red-400 shrink-0 mt-0.5" />
|
<XCircle className="w-5 h-5 text-red-400 shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-red-500">Deploy failed</p>
|
<p className="text-sm font-semibold text-red-500">部署失败</p>
|
||||||
<p className="text-sm text-red-400 mt-1 break-words">{error}</p>
|
<p className="text-sm text-red-400 mt-1 break-words">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -255,7 +255,7 @@ export default function CreatePage() {
|
|||||||
{/* Steps */}
|
{/* Steps */}
|
||||||
<div className="rounded-xl border border-[var(--border)] bg-[var(--bg-card)] overflow-hidden">
|
<div className="rounded-xl border border-[var(--border)] bg-[var(--bg-card)] overflow-hidden">
|
||||||
<div className="px-5 py-3 border-b border-[var(--border)] text-sm font-medium text-[var(--text-muted)]">
|
<div className="px-5 py-3 border-b border-[var(--border)] text-sm font-medium text-[var(--text-muted)]">
|
||||||
Pipeline Steps
|
执行步骤
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y divide-[var(--border-subtle)]">
|
<div className="divide-y divide-[var(--border-subtle)]">
|
||||||
{result.steps.map((s, i) => (
|
{result.steps.map((s, i) => (
|
||||||
@ -280,16 +280,16 @@ export default function CreatePage() {
|
|||||||
<div className="px-5 py-3 border-b border-[var(--border)] flex items-center justify-between">
|
<div className="px-5 py-3 border-b border-[var(--border)] flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2 text-sm font-medium text-[var(--text-muted)]">
|
<div className="flex items-center gap-2 text-sm font-medium text-[var(--text-muted)]">
|
||||||
<Terminal className="w-4 h-4" />
|
<Terminal className="w-4 h-4" />
|
||||||
Install Command
|
安装命令
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={copyCommand}
|
onClick={copyCommand}
|
||||||
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium transition bg-[var(--bg-subtle)] hover:bg-[var(--bg-card)] border border-[var(--border)] text-[var(--text-muted)]"
|
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium transition bg-[var(--bg-subtle)] hover:bg-[var(--bg-card)] border border-[var(--border)] text-[var(--text-muted)]"
|
||||||
>
|
>
|
||||||
{copied ? (
|
{copied ? (
|
||||||
<><Check className="w-3 h-3 text-emerald-500" />Copied</>
|
<><Check className="w-3 h-3 text-emerald-500" />已复制</>
|
||||||
) : (
|
) : (
|
||||||
<><Copy className="w-3 h-3" />Copy</>
|
<><Copy className="w-3 h-3" />复制</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -297,7 +297,7 @@ export default function CreatePage() {
|
|||||||
{result.installCommand}
|
{result.installCommand}
|
||||||
</pre>
|
</pre>
|
||||||
<div className="px-5 py-3 border-t border-[var(--border)] text-xs text-[var(--text-faint)]">
|
<div className="px-5 py-3 border-t border-[var(--border)] text-xs text-[var(--text-faint)]">
|
||||||
Paste on your server (requires root) — installs cloudflared, registers the tunnel, and configures SSH CA trust.
|
在服务器上执行(需要 root 权限)——自动安装 cloudflared、注册隆道、配置 SSH CA 信任。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -311,7 +311,7 @@ export default function CreatePage() {
|
|||||||
className="flex items-center justify-center gap-2 rounded-xl border border-[var(--accent-border)] bg-[var(--accent-dim)] p-4 text-emerald-600 dark:text-emerald-300 hover:bg-[var(--accent-dim)] transition font-medium"
|
className="flex items-center justify-center gap-2 rounded-xl border border-[var(--accent-border)] bg-[var(--accent-dim)] p-4 text-emerald-600 dark:text-emerald-300 hover:bg-[var(--accent-dim)] transition font-medium"
|
||||||
>
|
>
|
||||||
<ExternalLink className="w-4 h-4" />
|
<ExternalLink className="w-4 h-4" />
|
||||||
Open {result.launchUrl}
|
打开 {result.launchUrl}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -11,19 +11,19 @@ export default function LoginPage() {
|
|||||||
<div className="w-12 h-12 rounded-2xl bg-[var(--accent-dim)] border border-[var(--accent-border)] flex items-center justify-center mx-auto mb-5">
|
<div className="w-12 h-12 rounded-2xl bg-[var(--accent-dim)] border border-[var(--accent-border)] flex items-center justify-center mx-auto mb-5">
|
||||||
<Monitor className="w-6 h-6 text-emerald-500" />
|
<Monitor className="w-6 h-6 text-emerald-500" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-xl font-bold text-[var(--text)] mb-1">SSH Launcher</h1>
|
<h1 className="text-xl font-bold text-[var(--text)] mb-1">SSH 管理台</h1>
|
||||||
<p className="text-sm text-[var(--text-muted)] mb-8">
|
<p className="text-sm text-[var(--text-muted)] mb-8">
|
||||||
Sign in to manage your Cloudflare Zero Trust SSH hosts
|
登录以管理您的 Cloudflare Zero Trust SSH 主机
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => signIn("github", { callbackUrl: "/" })}
|
onClick={() => signIn("github", { callbackUrl: "/" })}
|
||||||
className="w-full flex items-center justify-center gap-3 rounded-xl bg-[var(--text)] text-[var(--bg)] font-semibold py-3 px-4 hover:opacity-90 transition"
|
className="w-full flex items-center justify-center gap-3 rounded-xl bg-[var(--text)] text-[var(--bg)] font-semibold py-3 px-4 hover:opacity-90 transition"
|
||||||
>
|
>
|
||||||
<Github className="w-4 h-4" />
|
<Github className="w-4 h-4" />
|
||||||
Continue with GitHub
|
使用 GitHub 登录
|
||||||
</button>
|
</button>
|
||||||
<p className="text-xs text-[var(--text-faint)] mt-6">
|
<p className="text-xs text-[var(--text-faint)] mt-6">
|
||||||
Your Cloudflare credentials are stored encrypted in your browser session.
|
您的 Cloudflare 凭证将加密存储在会话中。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -59,9 +59,9 @@ export default function SettingsPage() {
|
|||||||
<Settings className="w-5 h-5 text-[var(--text-muted)]" />
|
<Settings className="w-5 h-5 text-[var(--text-muted)]" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-[var(--text)]">Settings</h1>
|
<h1 className="text-xl font-bold text-[var(--text)]">设置</h1>
|
||||||
<p className="text-sm text-[var(--text-muted)]">
|
<p className="text-sm text-[var(--text-muted)]">
|
||||||
Cloudflare API credentials
|
Cloudflare API 凭证
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -81,18 +81,17 @@ export default function SettingsPage() {
|
|||||||
<div className="text-sm min-w-0">
|
<div className="text-sm min-w-0">
|
||||||
{state.configured ? (
|
{state.configured ? (
|
||||||
<>
|
<>
|
||||||
<p className="font-medium text-emerald-600 dark:text-emerald-400">Credentials configured</p>
|
<p className="font-medium text-emerald-600 dark:text-emerald-400">凭证已配置</p>
|
||||||
<p className="text-[var(--text-faint)] mt-0.5 font-mono text-xs break-all">
|
<p className="text-[var(--text-faint)] mt-0.5 font-mono text-xs break-all">
|
||||||
Account: {state.accountId}<br />
|
账户 ID:{state.accountId}<br />
|
||||||
Token: {state.apiTokenMasked}
|
令牌:{state.apiTokenMasked}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="font-medium text-amber-600 dark:text-amber-400">Not configured</p>
|
<p className="font-medium text-amber-600 dark:text-amber-400">尚未配置</p>
|
||||||
<p className="text-[var(--text-faint)] mt-0.5 text-xs">
|
<p className="text-[var(--text-faint)] mt-0.5 text-xs">
|
||||||
Enter your Cloudflare API credentials below. They'll be stored
|
请填入您的 Cloudflare API 凭证,将加密存储于 Redis(有效期 90 天)。
|
||||||
encrypted in your session cookie (90 days).
|
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -105,32 +104,32 @@ export default function SettingsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<label className="flex items-center gap-1.5 text-sm font-medium text-[var(--text)] mb-1.5">
|
<label className="flex items-center gap-1.5 text-sm font-medium text-[var(--text)] mb-1.5">
|
||||||
<Key className="w-3.5 h-3.5 text-[var(--text-muted)]" />
|
<Key className="w-3.5 h-3.5 text-[var(--text-muted)]" />
|
||||||
Account ID
|
账户 ID
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={accountId}
|
value={accountId}
|
||||||
onChange={(e) => setAccountId(e.target.value)}
|
onChange={(e) => setAccountId(e.target.value)}
|
||||||
placeholder="Your Cloudflare Account ID"
|
placeholder="您的 Cloudflare Account ID"
|
||||||
className="settings-input"
|
className="settings-input"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-[var(--text-faint)] mt-1">
|
<p className="text-xs text-[var(--text-faint)] mt-1">
|
||||||
Found in Cloudflare Dashboard → right sidebar
|
在 Cloudflare 控制台 → 右侧边栏可找到
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="flex items-center gap-1.5 text-sm font-medium text-[var(--text)] mb-1.5">
|
<label className="flex items-center gap-1.5 text-sm font-medium text-[var(--text)] mb-1.5">
|
||||||
<Key className="w-3.5 h-3.5 text-[var(--text-muted)]" />
|
<Key className="w-3.5 h-3.5 text-[var(--text-muted)]" />
|
||||||
API Token
|
API Token(令牌)
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type={showToken ? "text" : "password"}
|
type={showToken ? "text" : "password"}
|
||||||
value={apiToken}
|
value={apiToken}
|
||||||
onChange={(e) => setApiToken(e.target.value)}
|
onChange={(e) => setApiToken(e.target.value)}
|
||||||
placeholder={state?.configured ? "Enter new token to update" : "Your Cloudflare API Token"}
|
placeholder={state?.configured ? "输入新 Token 以更新" : "您的 Cloudflare API Token"}
|
||||||
className="settings-input pr-10"
|
className="settings-input pr-10"
|
||||||
required={!state?.configured}
|
required={!state?.configured}
|
||||||
/>
|
/>
|
||||||
@ -143,7 +142,7 @@ export default function SettingsPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-[var(--text-faint)] mt-1">
|
<p className="text-xs text-[var(--text-faint)] mt-1">
|
||||||
Needs: Access Write · Tunnel Edit · DNS Edit · Zone Read
|
所需权限:Access Write · Tunnel Edit · DNS Edit · Zone Read
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -160,11 +159,11 @@ export default function SettingsPage() {
|
|||||||
className="w-full flex items-center justify-center gap-2 rounded-xl bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 text-white font-semibold py-2.5 transition"
|
className="w-full flex items-center justify-center gap-2 rounded-xl bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 text-white font-semibold py-2.5 transition"
|
||||||
>
|
>
|
||||||
{saving ? (
|
{saving ? (
|
||||||
<><RefreshCw className="w-4 h-4 animate-spin" /> Saving...</>
|
<><RefreshCw className="w-4 h-4 animate-spin" /> 保存中...</>
|
||||||
) : saved ? (
|
) : saved ? (
|
||||||
<><Check className="w-4 h-4" /> Saved!</>
|
<><Check className="w-4 h-4" /> 已保存!</>
|
||||||
) : (
|
) : (
|
||||||
<><Save className="w-4 h-4" /> Save Credentials</>
|
<><Save className="w-4 h-4" /> 保存凭证</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -20,8 +20,8 @@ export default async function TunnelsPage() {
|
|||||||
<div className="min-h-[calc(100vh-3.5rem)] flex items-center justify-center p-4">
|
<div className="min-h-[calc(100vh-3.5rem)] flex items-center justify-center p-4">
|
||||||
<div className="max-w-md w-full rounded-2xl border border-red-500/20 bg-red-500/5 p-8 text-center">
|
<div className="max-w-md w-full rounded-2xl border border-red-500/20 bg-red-500/5 p-8 text-center">
|
||||||
<AlertTriangle className="w-10 h-10 text-red-400 mx-auto mb-4" />
|
<AlertTriangle className="w-10 h-10 text-red-400 mx-auto mb-4" />
|
||||||
<h2 className="text-lg font-semibold text-red-300 mb-2">
|
<h2 className="text-lg font-semibold text-red-400 mb-2">
|
||||||
Failed to load tunnels
|
加载隆道失败
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-red-400/80 break-words">{error}</p>
|
<p className="text-sm text-red-400/80 break-words">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -83,10 +83,10 @@ export default function Dashboard({ apps }: { apps: SshAppData[] }) {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold tracking-tight text-[var(--text)]">
|
<h1 className="text-2xl font-bold tracking-tight text-[var(--text)]">
|
||||||
SSH Launcher
|
SSH 管理台
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-[var(--text-muted)]">
|
<p className="text-sm text-[var(--text-muted)]">
|
||||||
Cloudflare Zero Trust · Browser Terminal
|
Cloudflare Zero Trust · 浏览器终端
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -96,23 +96,23 @@ export default function Dashboard({ apps }: { apps: SshAppData[] }) {
|
|||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8">
|
||||||
<StatCard
|
<StatCard
|
||||||
icon={<Server className="w-4 h-4" />}
|
icon={<Server className="w-4 h-4" />}
|
||||||
label="Total Hosts"
|
label="主机总数"
|
||||||
value={apps.length}
|
value={apps.length}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
icon={<Activity className="w-4 h-4" />}
|
icon={<Activity className="w-4 h-4" />}
|
||||||
label="Visible"
|
label="当前显示"
|
||||||
value={filtered.length}
|
value={filtered.length}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
icon={<Tag className="w-4 h-4" />}
|
icon={<Tag className="w-4 h-4" />}
|
||||||
label="Tags"
|
label="标签"
|
||||||
value={allTags.length}
|
value={allTags.length}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
icon={<Shield className="w-4 h-4" />}
|
icon={<Shield className="w-4 h-4" />}
|
||||||
label="Zero Trust"
|
label="零信任"
|
||||||
value="ON"
|
value="开启"
|
||||||
accent
|
accent
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -124,7 +124,7 @@ export default function Dashboard({ apps }: { apps: SshAppData[] }) {
|
|||||||
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-faint)]" />
|
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-faint)]" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search hosts by name, domain, or tag..."
|
placeholder="按名称、域名或标签搜索主机..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
className="w-full rounded-xl border border-[var(--border)] bg-[var(--bg-subtle)] pl-10 pr-4 py-2.5 text-sm text-[var(--text)] placeholder:text-[var(--text-faint)] outline-none transition focus:border-[var(--accent-border)] focus:ring-1 focus:ring-[var(--accent-dim)]"
|
className="w-full rounded-xl border border-[var(--border)] bg-[var(--bg-subtle)] pl-10 pr-4 py-2.5 text-sm text-[var(--text)] placeholder:text-[var(--text-faint)] outline-none transition focus:border-[var(--accent-border)] focus:ring-1 focus:ring-[var(--accent-dim)]"
|
||||||
@ -142,7 +142,7 @@ export default function Dashboard({ apps }: { apps: SshAppData[] }) {
|
|||||||
: "bg-zinc-800/50 text-zinc-500 border border-zinc-800 hover:border-zinc-700"
|
: "bg-zinc-800/50 text-zinc-500 border border-zinc-800 hover:border-zinc-700"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
All
|
全部
|
||||||
</button>
|
</button>
|
||||||
{allTags.map((tag) => (
|
{allTags.map((tag) => (
|
||||||
<button
|
<button
|
||||||
@ -170,12 +170,12 @@ export default function Dashboard({ apps }: { apps: SshAppData[] }) {
|
|||||||
<Terminal className="w-8 h-8 opacity-30" />
|
<Terminal className="w-8 h-8 opacity-30" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-lg font-medium text-[var(--text-muted)]">
|
<p className="text-lg font-medium text-[var(--text-muted)]">
|
||||||
No hosts found
|
未找到主机
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm mt-1">
|
<p className="text-sm mt-1">
|
||||||
{search || selectedTag
|
{search || selectedTag
|
||||||
? "Try adjusting your search or filter."
|
? "请调整搜索条件或过滤器"
|
||||||
: "Ensure your API token has Access: Apps and Policies Read permission."}
|
: "请确认 API Token 具备 Access: Apps and Policies Read 权限"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -191,11 +191,11 @@ export default function Dashboard({ apps }: { apps: SshAppData[] }) {
|
|||||||
<footer className="mt-14 pt-6 border-t border-[var(--border-subtle)] flex flex-col sm:flex-row items-center justify-between gap-2 text-xs text-[var(--text-faint)]">
|
<footer className="mt-14 pt-6 border-t border-[var(--border-subtle)] flex flex-col sm:flex-row items-center justify-between gap-2 text-xs text-[var(--text-faint)]">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Shield className="w-3 h-3" />
|
<Shield className="w-3 h-3" />
|
||||||
<span>Protected by Cloudflare Zero Trust</span>
|
<span>由 Cloudflare Zero Trust 保护</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Clock className="w-3 h-3" />
|
<Clock className="w-3 h-3" />
|
||||||
<span>Auto-refreshes every 60s</span>
|
<span>每 60 秒自动刷新</span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -60,7 +60,7 @@ export default function MetricsWidget({ metricsUrl }: { metricsUrl: string }) {
|
|||||||
return (
|
return (
|
||||||
<div className="mt-3 pt-3 border-t border-[var(--border)] flex items-center gap-2 text-xs text-[var(--text-faint)]">
|
<div className="mt-3 pt-3 border-t border-[var(--border)] flex items-center gap-2 text-xs text-[var(--text-faint)]">
|
||||||
<Activity className="w-3 h-3" />
|
<Activity className="w-3 h-3" />
|
||||||
<span>Metrics agent unreachable</span>
|
<span>探针不可达</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -90,7 +90,7 @@ export default function MetricsWidget({ metricsUrl }: { metricsUrl: string }) {
|
|||||||
</div>
|
</div>
|
||||||
<Bar value={data.cpu} />
|
<Bar value={data.cpu} />
|
||||||
<p className="text-[10px] text-[var(--text-faint)]">
|
<p className="text-[10px] text-[var(--text-faint)]">
|
||||||
load {data.load.map((l) => l.toFixed(2)).join(" ")}
|
负载 {data.load.map((l) => l.toFixed(2)).join(" ")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@ -159,11 +159,11 @@ export default function MetricsWidget({ metricsUrl }: { metricsUrl: string }) {
|
|||||||
{/* Uptime */}
|
{/* Uptime */}
|
||||||
<div className="flex items-center justify-between text-[10px] text-[var(--text-faint)]">
|
<div className="flex items-center justify-between text-[10px] text-[var(--text-faint)]">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Clock className="w-3 h-3" /> up {fmtUptime(data.uptime)}
|
<Clock className="w-3 h-3" /> 运行 {fmtUptime(data.uptime)}
|
||||||
</span>
|
</span>
|
||||||
{lastUpdate && (
|
{lastUpdate && (
|
||||||
<span>
|
<span>
|
||||||
{Math.round((Date.now() - lastUpdate) / 1000)}s ago
|
{Math.round((Date.now() - lastUpdate) / 1000)} 秒前更新
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,10 +8,10 @@ import { useSession, signIn, signOut } from "next-auth/react";
|
|||||||
import { Monitor, PlusCircle, Server, Sun, Moon, Settings, LogOut, LogIn } from "lucide-react";
|
import { Monitor, PlusCircle, Server, Sun, Moon, Settings, LogOut, LogIn } from "lucide-react";
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
{ href: "/", label: "Hosts", icon: Monitor },
|
{ href: "/", label: "主机", icon: Monitor },
|
||||||
{ href: "/create", label: "Deploy", icon: PlusCircle },
|
{ href: "/create", label: "部署", icon: PlusCircle },
|
||||||
{ href: "/tunnels", label: "Tunnels", icon: Server },
|
{ href: "/tunnels", label: "隧道", icon: Server },
|
||||||
{ href: "/settings", label: "Settings", icon: Settings },
|
{ href: "/settings", label: "设置", icon: Settings },
|
||||||
];
|
];
|
||||||
|
|
||||||
function ThemeToggle() {
|
function ThemeToggle() {
|
||||||
@ -41,7 +41,7 @@ function UserWidget() {
|
|||||||
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-medium transition"
|
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-medium transition"
|
||||||
>
|
>
|
||||||
<LogIn className="w-3.5 h-3.5" />
|
<LogIn className="w-3.5 h-3.5" />
|
||||||
<span className="hidden sm:inline">Sign in</span>
|
<span className="hidden sm:inline">登录</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -61,7 +61,7 @@ function UserWidget() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => signOut({ callbackUrl: "/login" })}
|
onClick={() => signOut({ callbackUrl: "/login" })}
|
||||||
className="w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-faint)] hover:text-red-500 hover:bg-[var(--bg-card)] transition"
|
className="w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-faint)] hover:text-red-500 hover:bg-[var(--bg-card)] transition"
|
||||||
title="Sign out"
|
title="退出登录"
|
||||||
>
|
>
|
||||||
<LogOut className="w-3.5 h-3.5" />
|
<LogOut className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@ -82,7 +82,7 @@ export default function Navigation() {
|
|||||||
<div className="w-7 h-7 rounded-lg bg-[var(--accent-dim)] border border-[var(--accent-border)] flex items-center justify-center">
|
<div className="w-7 h-7 rounded-lg bg-[var(--accent-dim)] border border-[var(--accent-border)] flex items-center justify-center">
|
||||||
<Monitor className="w-3.5 h-3.5 text-emerald-500" />
|
<Monitor className="w-3.5 h-3.5 text-emerald-500" />
|
||||||
</div>
|
</div>
|
||||||
<span className="hidden sm:inline text-sm">SSH Launcher</span>
|
<span className="hidden sm:inline text-sm">SSH 管理台</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex items-center gap-1 flex-1">
|
<div className="flex items-center gap-1 flex-1">
|
||||||
|
|||||||
@ -30,13 +30,13 @@ interface Tunnel {
|
|||||||
function timeAgo(dateStr: string): string {
|
function timeAgo(dateStr: string): string {
|
||||||
const diff = Date.now() - new Date(dateStr).getTime();
|
const diff = Date.now() - new Date(dateStr).getTime();
|
||||||
const mins = Math.floor(diff / 60000);
|
const mins = Math.floor(diff / 60000);
|
||||||
if (mins < 1) return "Just now";
|
if (mins < 1) return "刚刚";
|
||||||
if (mins < 60) return `${mins}m ago`;
|
if (mins < 60) return `${mins} 分钟前`;
|
||||||
const hrs = Math.floor(mins / 60);
|
const hrs = Math.floor(mins / 60);
|
||||||
if (hrs < 24) return `${hrs}h ago`;
|
if (hrs < 24) return `${hrs} 小时前`;
|
||||||
const days = Math.floor(hrs / 24);
|
const days = Math.floor(hrs / 24);
|
||||||
if (days < 30) return `${days}d ago`;
|
if (days < 30) return `${days} 天前`;
|
||||||
return `${Math.floor(days / 30)}mo ago`;
|
return `${Math.floor(days / 30)} 个月前`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
|
export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
|
||||||
@ -57,43 +57,42 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[calc(100vh-3.5rem)]">
|
<div className="min-h-[calc(100vh-3.5rem)]">
|
||||||
<div className="fixed inset-0 -z-10">
|
<div className="fixed inset-0 -z-10 pointer-events-none">
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(16,185,129,0.08),transparent)]" />
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,var(--accent-dim),transparent)]" />
|
||||||
<div className="absolute inset-0 bg-zinc-950" style={{ zIndex: -1 }} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-3 mb-8">
|
<div className="flex items-center gap-3 mb-8">
|
||||||
<div className="w-10 h-10 rounded-xl bg-blue-500/10 border border-blue-500/20 flex items-center justify-center">
|
<div className="w-10 h-10 rounded-xl bg-[var(--accent-dim)] border border-[var(--accent-border)] flex items-center justify-center">
|
||||||
<Server className="w-5 h-5 text-blue-400" />
|
<Server className="w-5 h-5 text-emerald-500" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-white">Tunnels</h1>
|
<h1 className="text-2xl font-bold text-[var(--text)]">隆道列表</h1>
|
||||||
<p className="text-sm text-zinc-500">
|
<p className="text-sm text-[var(--text-muted)]">
|
||||||
{tunnels.length} total · {activeCount} active
|
共 {tunnels.length} 条 · {activeCount} 条在线
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="relative mb-6">
|
<div className="relative mb-6">
|
||||||
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500" />
|
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-faint)]" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search by name or ID..."
|
placeholder="按名称或 ID 搜索..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
className="w-full rounded-xl border border-zinc-800 bg-zinc-900/80 backdrop-blur-sm pl-10 pr-4 py-2.5 text-sm text-zinc-200 placeholder-zinc-600 outline-none transition focus:border-blue-500/50 focus:ring-1 focus:ring-blue-500/20"
|
className="w-full rounded-xl border border-[var(--border)] bg-[var(--bg-subtle)] pl-10 pr-4 py-2.5 text-sm text-[var(--text)] placeholder:text-[var(--text-faint)] outline-none transition focus:border-[var(--accent-border)] focus:ring-1 focus:ring-[var(--accent-dim)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Empty */}
|
{/* Empty */}
|
||||||
{filtered.length === 0 && (
|
{filtered.length === 0 && (
|
||||||
<div className="flex flex-col items-center justify-center py-24 text-zinc-500">
|
<div className="flex flex-col items-center justify-center py-24 text-[var(--text-faint)]">
|
||||||
<Server className="w-12 h-12 opacity-30 mb-4" />
|
<Server className="w-12 h-12 opacity-30 mb-4" />
|
||||||
<p className="text-lg font-medium text-zinc-400">
|
<p className="text-lg font-medium text-[var(--text-muted)]">
|
||||||
No tunnels found
|
未找到隆道
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -106,7 +105,7 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={tunnel.id}
|
key={tunnel.id}
|
||||||
className="rounded-xl border border-zinc-800/80 bg-zinc-900/60 backdrop-blur-sm p-5 transition hover:border-zinc-700"
|
className="rounded-xl border border-[var(--border)] bg-[var(--bg-card)] p-5 transition hover:border-[var(--accent-border)]"
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
{/* Left */}
|
{/* Left */}
|
||||||
@ -117,45 +116,45 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
|
|||||||
) : (
|
) : (
|
||||||
<WifiOff className="w-4 h-4 text-zinc-600 shrink-0" />
|
<WifiOff className="w-4 h-4 text-zinc-600 shrink-0" />
|
||||||
)}
|
)}
|
||||||
<span className="font-semibold text-white truncate">
|
<span className="font-semibold text-[var(--text)] truncate">
|
||||||
{tunnel.name}
|
{tunnel.name}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||||
isActive
|
isActive
|
||||||
? "bg-emerald-500/10 text-emerald-400 border border-emerald-500/20"
|
? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20"
|
||||||
: "bg-zinc-800 text-zinc-500 border border-zinc-700"
|
: "bg-[var(--bg-subtle)] text-[var(--text-faint)] border border-[var(--border)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isActive ? "Active" : "Inactive"}
|
{isActive ? "在线" : "离线"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-zinc-600 font-mono truncate">
|
<p className="text-xs text-[var(--text-faint)] font-mono truncate">
|
||||||
{tunnel.id}
|
{tunnel.id}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right meta */}
|
{/* Right meta */}
|
||||||
<div className="text-right text-xs text-zinc-600 shrink-0">
|
<div className="text-right text-xs text-[var(--text-faint)] shrink-0">
|
||||||
<div className="flex items-center gap-1 justify-end">
|
<div className="flex items-center gap-1 justify-end">
|
||||||
<Clock className="w-3 h-3" />
|
<Clock className="w-3 h-3" />
|
||||||
Created {timeAgo(tunnel.created_at)}
|
创建于 {timeAgo(tunnel.created_at)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Connections */}
|
{/* Connections */}
|
||||||
{isActive && tunnel.connections.length > 0 && (
|
{isActive && tunnel.connections.length > 0 && (
|
||||||
<div className="mt-3 pt-3 border-t border-zinc-800/50 flex flex-wrap gap-2">
|
<div className="mt-3 pt-3 border-t border-[var(--border)] flex flex-wrap gap-2">
|
||||||
{tunnel.connections.map((conn) => (
|
{tunnel.connections.map((conn) => (
|
||||||
<div
|
<div
|
||||||
key={conn.id}
|
key={conn.id}
|
||||||
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-zinc-800/50 text-xs text-zinc-400"
|
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-[var(--bg-subtle)] text-xs text-[var(--text-muted)]"
|
||||||
>
|
>
|
||||||
<MapPin className="w-3 h-3 text-zinc-600" />
|
<MapPin className="w-3 h-3 text-[var(--text-faint)]" />
|
||||||
{conn.origin_ip}
|
{conn.origin_ip}
|
||||||
<span className="text-zinc-600">·</span>
|
<span className="text-[var(--text-faint)]">·</span>
|
||||||
<span className="text-zinc-600">
|
<span className="text-[var(--text-faint)]">
|
||||||
{timeAgo(conn.opened_at)}
|
{timeAgo(conn.opened_at)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -168,9 +167,9 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="mt-14 pt-6 border-t border-zinc-900 flex items-center gap-2 text-xs text-zinc-600">
|
<div className="mt-14 pt-6 border-t border-[var(--border-subtle)] flex items-center gap-2 text-xs text-[var(--text-faint)]">
|
||||||
<Shield className="w-3 h-3" />
|
<Shield className="w-3 h-3" />
|
||||||
<span>Cloudflare Tunnel · auto-refreshes every 30s</span>
|
<span>Cloudflare 隆道 · 每 30 秒自动刷新</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -532,28 +532,32 @@ export async function runFullSetup(input: SetupInput): Promise<SetupResult> {
|
|||||||
|
|
||||||
// Build install command
|
// Build install command
|
||||||
const loginUser = input.ssoEmail.split("@")[0];
|
const loginUser = input.ssoEmail.split("@")[0];
|
||||||
|
const SCRIPT_URL =
|
||||||
|
"https://raw.githubusercontent.com/chunzhimoe/cf-status/main/setup-cf-browser-ssh.sh";
|
||||||
let installCommand: string | null = null;
|
let installCommand: string | null = null;
|
||||||
if (tunnelToken) {
|
if (tunnelToken) {
|
||||||
const parts = [
|
const parts = [
|
||||||
`sudo bash setup-cf-browser-ssh.sh`,
|
`curl -fsSL ${SCRIPT_URL} \\`,
|
||||||
` --tunnel-token "${tunnelToken}"`,
|
` | sudo bash -s -- \\`,
|
||||||
` --sso-email "${input.ssoEmail}"`,
|
` --tunnel-token "${tunnelToken}" \\`,
|
||||||
|
` --sso-email "${input.ssoEmail}" \\`,
|
||||||
` --login-user "${loginUser}"`,
|
` --login-user "${loginUser}"`,
|
||||||
];
|
];
|
||||||
if (caPubkey) {
|
if (caPubkey) {
|
||||||
|
parts[parts.length - 1] += ` \\`;
|
||||||
parts.push(` --ca-pubkey "${caPubkey}"`);
|
parts.push(` --ca-pubkey "${caPubkey}"`);
|
||||||
}
|
}
|
||||||
installCommand = parts.join(" \\\n");
|
installCommand = parts.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
const launchUrl = `https://${input.hostname}`;
|
const launchUrl = `https://${input.hostname}`;
|
||||||
|
|
||||||
// Browser rendering reminder
|
// 浏览器渲染提醒
|
||||||
steps.push({
|
steps.push({
|
||||||
step: "browser_rendering",
|
step: "browser_rendering",
|
||||||
status: "skipped",
|
status: "skipped",
|
||||||
message:
|
message:
|
||||||
"Remember: enable Browser rendering → SSH in the Cloudflare dashboard for this Access app",
|
"请在 Cloudflare 控制台 → Access App → 编辑 → 启用「Browser rendering → SSH」",
|
||||||
});
|
});
|
||||||
|
|
||||||
return { steps, tunnelToken, caPubkey, installCommand, launchUrl };
|
return { steps, tunnelToken, caPubkey, installCommand, launchUrl };
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user