feat: 全界面中文汉化 + 安装命令改为 curl 脚本
This commit is contained in:
parent
c6a57b5551
commit
28a6f4bc45
@ -39,14 +39,14 @@ interface Zone {
|
||||
}
|
||||
|
||||
const STEP_LABELS: Record<string, string> = {
|
||||
find_zone: "Lookup DNS Zone",
|
||||
create_tunnel: "Create Tunnel",
|
||||
configure_tunnel: "Configure Ingress",
|
||||
create_dns: "Create DNS Record",
|
||||
create_access_app: "Create Access App",
|
||||
setup_ca: "Setup Short-lived Cert",
|
||||
get_token: "Get Tunnel Token",
|
||||
browser_rendering: "Browser Rendering",
|
||||
find_zone: "查找 DNS Zone",
|
||||
create_tunnel: "创建隆道",
|
||||
configure_tunnel: "配置 Ingress",
|
||||
create_dns: "创建 DNS 记录",
|
||||
create_access_app: "创建访问应用",
|
||||
setup_ca: "配置短期证书",
|
||||
get_token: "获取隆道 Token",
|
||||
browser_rendering: "浏览器渲染提醒",
|
||||
};
|
||||
|
||||
export default function CreatePage() {
|
||||
@ -143,24 +143,24 @@ export default function CreatePage() {
|
||||
<Rocket className="w-5 h-5 text-emerald-500" />
|
||||
</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)]">
|
||||
One form → Tunnel + DNS + Access App + Install Command
|
||||
一键完成 → 隆道 + DNS + 访问应用 + 安装命令
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<Field icon={<Server className="w-4 h-4" />} label="Server Name"
|
||||
hint="A friendly label for this server, e.g. prod-web-1">
|
||||
<Field icon={<Server className="w-4 h-4" />} label="服务器名称"
|
||||
hint="服务器昵称,如 prod-web-1">
|
||||
<input type="text" value={form.serverName} onChange={set("serverName")}
|
||||
placeholder="prod-web-1" className="form-input" required />
|
||||
</Field>
|
||||
|
||||
{/* SSH Hostname — zone dropdown + subdomain */}
|
||||
<Field icon={<Globe className="w-4 h-4" />} label="SSH Hostname"
|
||||
hint={hostname ? `Will create: ${hostname}` : "Select a zone, then enter a subdomain"}>
|
||||
<Field icon={<Globe className="w-4 h-4" />} label="SSH 访问域名"
|
||||
hint={hostname ? `将创建:${hostname}` : "选择 Zone 再填写子域名前缀"}>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<input
|
||||
@ -177,11 +177,11 @@ export default function CreatePage() {
|
||||
{zonesLoading ? (
|
||||
<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" />
|
||||
<span className="text-sm">Loading zones…</span>
|
||||
<span className="text-sm">加载 Zone 中…</span>
|
||||
</div>
|
||||
) : zonesError ? (
|
||||
<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)]">
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@ -194,7 +194,7 @@ export default function CreatePage() {
|
||||
className="form-input appearance-none pr-8"
|
||||
required
|
||||
>
|
||||
<option value="">Select zone…</option>
|
||||
<option value="">选择 Zone…</option>
|
||||
{zones.map((z) => (
|
||||
<option key={z.id} value={z.id}>{z.name}</option>
|
||||
))}
|
||||
@ -206,20 +206,20 @@ export default function CreatePage() {
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
<Field icon={<Hash className="w-4 h-4" />} label="SSH Port"
|
||||
hint="Server-side SSH port (usually 22)">
|
||||
<Field icon={<Hash className="w-4 h-4" />} label="SSH 端口"
|
||||
hint="服务器 SSH 端口(通常为 22)">
|
||||
<input type="number" value={form.sshPort} onChange={set("sshPort")}
|
||||
placeholder="22" className="form-input w-28" />
|
||||
</Field>
|
||||
|
||||
<Field icon={<Mail className="w-4 h-4" />} label="SSO Email"
|
||||
hint="Your Cloudflare SSO email — its prefix becomes the Linux user">
|
||||
<Field icon={<Mail className="w-4 h-4" />} label="SSO 邮筱"
|
||||
hint="Cloudflare SSO 邮筱,前缀将作为 Linux 用户名">
|
||||
<input type="email" value={form.ssoEmail} onChange={set("ssoEmail")}
|
||||
placeholder="admin@example.com" className="form-input" required />
|
||||
</Field>
|
||||
|
||||
<Field icon={<Mail className="w-4 h-4" />} label="Allowed Emails (optional)"
|
||||
hint="Comma-separated emails for the Access Policy. Leave empty = any authenticated user">
|
||||
<Field icon={<Mail className="w-4 h-4" />} label="允许访问的邮筱(可选)"
|
||||
hint="多个邮筱用逗号分隔,留空表示所有已认证用户均可访问">
|
||||
<textarea value={form.allowedEmails} onChange={set("allowedEmails")}
|
||||
placeholder="alice@example.com, bob@example.com"
|
||||
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"
|
||||
>
|
||||
{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>
|
||||
</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">
|
||||
<XCircle className="w-5 h-5 text-red-400 shrink-0 mt-0.5" />
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -255,7 +255,7 @@ export default function CreatePage() {
|
||||
{/* Steps */}
|
||||
<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)]">
|
||||
Pipeline Steps
|
||||
执行步骤
|
||||
</div>
|
||||
<div className="divide-y divide-[var(--border-subtle)]">
|
||||
{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="flex items-center gap-2 text-sm font-medium text-[var(--text-muted)]">
|
||||
<Terminal className="w-4 h-4" />
|
||||
Install Command
|
||||
安装命令
|
||||
</div>
|
||||
<button
|
||||
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)]"
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
@ -297,7 +297,7 @@ export default function CreatePage() {
|
||||
{result.installCommand}
|
||||
</pre>
|
||||
<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>
|
||||
)}
|
||||
@ -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"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
Open {result.launchUrl}
|
||||
打开 {result.launchUrl}
|
||||
</a>
|
||||
)}
|
||||
</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">
|
||||
<Monitor className="w-6 h-6 text-emerald-500" />
|
||||
</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">
|
||||
Sign in to manage your Cloudflare Zero Trust SSH hosts
|
||||
登录以管理您的 Cloudflare Zero Trust SSH 主机
|
||||
</p>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Github className="w-4 h-4" />
|
||||
Continue with GitHub
|
||||
使用 GitHub 登录
|
||||
</button>
|
||||
<p className="text-xs text-[var(--text-faint)] mt-6">
|
||||
Your Cloudflare credentials are stored encrypted in your browser session.
|
||||
您的 Cloudflare 凭证将加密存储在会话中。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -59,9 +59,9 @@ export default function SettingsPage() {
|
||||
<Settings className="w-5 h-5 text-[var(--text-muted)]" />
|
||||
</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)]">
|
||||
Cloudflare API credentials
|
||||
Cloudflare API 凭证
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -81,18 +81,17 @@ export default function SettingsPage() {
|
||||
<div className="text-sm min-w-0">
|
||||
{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">
|
||||
Account: {state.accountId}<br />
|
||||
Token: {state.apiTokenMasked}
|
||||
账户 ID:{state.accountId}<br />
|
||||
令牌:{state.apiTokenMasked}
|
||||
</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">
|
||||
Enter your Cloudflare API credentials below. They'll be stored
|
||||
encrypted in your session cookie (90 days).
|
||||
请填入您的 Cloudflare API 凭证,将加密存储于 Redis(有效期 90 天)。
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
@ -105,32 +104,32 @@ export default function SettingsPage() {
|
||||
<div>
|
||||
<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)]" />
|
||||
Account ID
|
||||
账户 ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={accountId}
|
||||
onChange={(e) => setAccountId(e.target.value)}
|
||||
placeholder="Your Cloudflare Account ID"
|
||||
placeholder="您的 Cloudflare Account ID"
|
||||
className="settings-input"
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-[var(--text-faint)] mt-1">
|
||||
Found in Cloudflare Dashboard → right sidebar
|
||||
在 Cloudflare 控制台 → 右侧边栏可找到
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<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)]" />
|
||||
API Token
|
||||
API Token(令牌)
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showToken ? "text" : "password"}
|
||||
value={apiToken}
|
||||
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"
|
||||
required={!state?.configured}
|
||||
/>
|
||||
@ -143,7 +142,7 @@ export default function SettingsPage() {
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
</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"
|
||||
>
|
||||
{saving ? (
|
||||
<><RefreshCw className="w-4 h-4 animate-spin" /> Saving...</>
|
||||
<><RefreshCw className="w-4 h-4 animate-spin" /> 保存中...</>
|
||||
) : 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>
|
||||
</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="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" />
|
||||
<h2 className="text-lg font-semibold text-red-300 mb-2">
|
||||
Failed to load tunnels
|
||||
<h2 className="text-lg font-semibold text-red-400 mb-2">
|
||||
加载隆道失败
|
||||
</h2>
|
||||
<p className="text-sm text-red-400/80 break-words">{error}</p>
|
||||
</div>
|
||||
|
||||
@ -83,10 +83,10 @@ export default function Dashboard({ apps }: { apps: SshAppData[] }) {
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-[var(--text)]">
|
||||
SSH Launcher
|
||||
SSH 管理台
|
||||
</h1>
|
||||
<p className="text-sm text-[var(--text-muted)]">
|
||||
Cloudflare Zero Trust · Browser Terminal
|
||||
Cloudflare Zero Trust · 浏览器终端
|
||||
</p>
|
||||
</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">
|
||||
<StatCard
|
||||
icon={<Server className="w-4 h-4" />}
|
||||
label="Total Hosts"
|
||||
label="主机总数"
|
||||
value={apps.length}
|
||||
/>
|
||||
<StatCard
|
||||
icon={<Activity className="w-4 h-4" />}
|
||||
label="Visible"
|
||||
label="当前显示"
|
||||
value={filtered.length}
|
||||
/>
|
||||
<StatCard
|
||||
icon={<Tag className="w-4 h-4" />}
|
||||
label="Tags"
|
||||
label="标签"
|
||||
value={allTags.length}
|
||||
/>
|
||||
<StatCard
|
||||
icon={<Shield className="w-4 h-4" />}
|
||||
label="Zero Trust"
|
||||
value="ON"
|
||||
label="零信任"
|
||||
value="开启"
|
||||
accent
|
||||
/>
|
||||
</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)]" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search hosts by name, domain, or tag..."
|
||||
placeholder="按名称、域名或标签搜索主机..."
|
||||
value={search}
|
||||
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)]"
|
||||
@ -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"
|
||||
}`}
|
||||
>
|
||||
All
|
||||
全部
|
||||
</button>
|
||||
{allTags.map((tag) => (
|
||||
<button
|
||||
@ -170,12 +170,12 @@ export default function Dashboard({ apps }: { apps: SshAppData[] }) {
|
||||
<Terminal className="w-8 h-8 opacity-30" />
|
||||
</div>
|
||||
<p className="text-lg font-medium text-[var(--text-muted)]">
|
||||
No hosts found
|
||||
未找到主机
|
||||
</p>
|
||||
<p className="text-sm mt-1">
|
||||
{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>
|
||||
</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)]">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Shield className="w-3 h-3" />
|
||||
<span>Protected by Cloudflare Zero Trust</span>
|
||||
<span>由 Cloudflare Zero Trust 保护</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>Auto-refreshes every 60s</span>
|
||||
<span>每 60 秒自动刷新</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@ -60,7 +60,7 @@ export default function MetricsWidget({ metricsUrl }: { metricsUrl: string }) {
|
||||
return (
|
||||
<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" />
|
||||
<span>Metrics agent unreachable</span>
|
||||
<span>探针不可达</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -90,7 +90,7 @@ export default function MetricsWidget({ metricsUrl }: { metricsUrl: string }) {
|
||||
</div>
|
||||
<Bar value={data.cpu} />
|
||||
<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>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
@ -159,11 +159,11 @@ export default function MetricsWidget({ metricsUrl }: { metricsUrl: string }) {
|
||||
{/* Uptime */}
|
||||
<div className="flex items-center justify-between text-[10px] text-[var(--text-faint)]">
|
||||
<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>
|
||||
{lastUpdate && (
|
||||
<span>
|
||||
{Math.round((Date.now() - lastUpdate) / 1000)}s ago
|
||||
{Math.round((Date.now() - lastUpdate) / 1000)} 秒前更新
|
||||
</span>
|
||||
)}
|
||||
</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";
|
||||
|
||||
const links = [
|
||||
{ href: "/", label: "Hosts", icon: Monitor },
|
||||
{ href: "/create", label: "Deploy", icon: PlusCircle },
|
||||
{ href: "/tunnels", label: "Tunnels", icon: Server },
|
||||
{ href: "/settings", label: "Settings", icon: Settings },
|
||||
{ href: "/", label: "主机", icon: Monitor },
|
||||
{ href: "/create", label: "部署", icon: PlusCircle },
|
||||
{ href: "/tunnels", label: "隧道", icon: Server },
|
||||
{ href: "/settings", label: "设置", icon: Settings },
|
||||
];
|
||||
|
||||
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"
|
||||
>
|
||||
<LogIn className="w-3.5 h-3.5" />
|
||||
<span className="hidden sm:inline">Sign in</span>
|
||||
<span className="hidden sm:inline">登录</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@ -61,7 +61,7 @@ function UserWidget() {
|
||||
<button
|
||||
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"
|
||||
title="Sign out"
|
||||
title="退出登录"
|
||||
>
|
||||
<LogOut className="w-3.5 h-3.5" />
|
||||
</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">
|
||||
<Monitor className="w-3.5 h-3.5 text-emerald-500" />
|
||||
</div>
|
||||
<span className="hidden sm:inline text-sm">SSH Launcher</span>
|
||||
<span className="hidden sm:inline text-sm">SSH 管理台</span>
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center gap-1 flex-1">
|
||||
|
||||
@ -30,13 +30,13 @@ interface Tunnel {
|
||||
function timeAgo(dateStr: string): string {
|
||||
const diff = Date.now() - new Date(dateStr).getTime();
|
||||
const mins = Math.floor(diff / 60000);
|
||||
if (mins < 1) return "Just now";
|
||||
if (mins < 60) return `${mins}m ago`;
|
||||
if (mins < 1) return "刚刚";
|
||||
if (mins < 60) return `${mins} 分钟前`;
|
||||
const hrs = Math.floor(mins / 60);
|
||||
if (hrs < 24) return `${hrs}h ago`;
|
||||
if (hrs < 24) return `${hrs} 小时前`;
|
||||
const days = Math.floor(hrs / 24);
|
||||
if (days < 30) return `${days}d ago`;
|
||||
return `${Math.floor(days / 30)}mo ago`;
|
||||
if (days < 30) return `${days} 天前`;
|
||||
return `${Math.floor(days / 30)} 个月前`;
|
||||
}
|
||||
|
||||
export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
|
||||
@ -57,43 +57,42 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
|
||||
|
||||
return (
|
||||
<div className="min-h-[calc(100vh-3.5rem)]">
|
||||
<div className="fixed inset-0 -z-10">
|
||||
<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-zinc-950" style={{ zIndex: -1 }} />
|
||||
<div className="fixed inset-0 -z-10 pointer-events-none">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,var(--accent-dim),transparent)]" />
|
||||
</div>
|
||||
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
||||
{/* Header */}
|
||||
<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">
|
||||
<Server className="w-5 h-5 text-blue-400" />
|
||||
<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-emerald-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">Tunnels</h1>
|
||||
<p className="text-sm text-zinc-500">
|
||||
{tunnels.length} total · {activeCount} active
|
||||
<h1 className="text-2xl font-bold text-[var(--text)]">隆道列表</h1>
|
||||
<p className="text-sm text-[var(--text-muted)]">
|
||||
共 {tunnels.length} 条 · {activeCount} 条在线
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<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
|
||||
type="text"
|
||||
placeholder="Search by name or ID..."
|
||||
placeholder="按名称或 ID 搜索..."
|
||||
value={search}
|
||||
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>
|
||||
|
||||
{/* Empty */}
|
||||
{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" />
|
||||
<p className="text-lg font-medium text-zinc-400">
|
||||
No tunnels found
|
||||
<p className="text-lg font-medium text-[var(--text-muted)]">
|
||||
未找到隆道
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@ -106,7 +105,7 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
|
||||
return (
|
||||
<div
|
||||
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">
|
||||
{/* Left */}
|
||||
@ -117,45 +116,45 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
|
||||
) : (
|
||||
<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}
|
||||
</span>
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
isActive
|
||||
? "bg-emerald-500/10 text-emerald-400 border border-emerald-500/20"
|
||||
: "bg-zinc-800 text-zinc-500 border border-zinc-700"
|
||||
? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20"
|
||||
: "bg-[var(--bg-subtle)] text-[var(--text-faint)] border border-[var(--border)]"
|
||||
}`}
|
||||
>
|
||||
{isActive ? "Active" : "Inactive"}
|
||||
{isActive ? "在线" : "离线"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-zinc-600 font-mono truncate">
|
||||
<p className="text-xs text-[var(--text-faint)] font-mono truncate">
|
||||
{tunnel.id}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<Clock className="w-3 h-3" />
|
||||
Created {timeAgo(tunnel.created_at)}
|
||||
创建于 {timeAgo(tunnel.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Connections */}
|
||||
{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) => (
|
||||
<div
|
||||
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}
|
||||
<span className="text-zinc-600">·</span>
|
||||
<span className="text-zinc-600">
|
||||
<span className="text-[var(--text-faint)]">·</span>
|
||||
<span className="text-[var(--text-faint)]">
|
||||
{timeAgo(conn.opened_at)}
|
||||
</span>
|
||||
</div>
|
||||
@ -168,9 +167,9 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
|
||||
</div>
|
||||
|
||||
{/* 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" />
|
||||
<span>Cloudflare Tunnel · auto-refreshes every 30s</span>
|
||||
<span>Cloudflare 隆道 · 每 30 秒自动刷新</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -532,28 +532,32 @@ export async function runFullSetup(input: SetupInput): Promise<SetupResult> {
|
||||
|
||||
// Build install command
|
||||
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;
|
||||
if (tunnelToken) {
|
||||
const parts = [
|
||||
`sudo bash setup-cf-browser-ssh.sh`,
|
||||
` --tunnel-token "${tunnelToken}"`,
|
||||
` --sso-email "${input.ssoEmail}"`,
|
||||
`curl -fsSL ${SCRIPT_URL} \\`,
|
||||
` | sudo bash -s -- \\`,
|
||||
` --tunnel-token "${tunnelToken}" \\`,
|
||||
` --sso-email "${input.ssoEmail}" \\`,
|
||||
` --login-user "${loginUser}"`,
|
||||
];
|
||||
if (caPubkey) {
|
||||
parts[parts.length - 1] += ` \\`;
|
||||
parts.push(` --ca-pubkey "${caPubkey}"`);
|
||||
}
|
||||
installCommand = parts.join(" \\\n");
|
||||
installCommand = parts.join("\n");
|
||||
}
|
||||
|
||||
const launchUrl = `https://${input.hostname}`;
|
||||
|
||||
// Browser rendering reminder
|
||||
// 浏览器渲染提醒
|
||||
steps.push({
|
||||
step: "browser_rendering",
|
||||
status: "skipped",
|
||||
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 };
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user