From 44744cf279915ba97dd2eb60a013c9d34c5b6010 Mon Sep 17 00:00:00 2001 From: 6zev Date: Thu, 3 Sep 2026 04:27:30 +0000 Subject: [PATCH] fix: Unauthorized on 13.140.132.105 - use SDK.fetchJSON + profile forwarding - frontend used raw fetch without auth -> 401 Unauthorized behind nginx on /api/plugins/* - now uses SDK.fetchJSON (auth-aware) first, fallback raw fetch with ?profile= forwarded from window.location - fetchJSONAllowError returns JSON even on 401 so UI shows detail instead of generic error - bump v2.0.2 --- dashboard/dist/index.js | 95 +++++++++++++++++++++++++++++++++++------ dashboard/manifest.json | 2 +- plugin.yaml | 2 +- 3 files changed, 83 insertions(+), 16 deletions(-) diff --git a/dashboard/dist/index.js b/dashboard/dist/index.js index a669d34..0c8fc41 100644 --- a/dashboard/dist/index.js +++ b/dashboard/dist/index.js @@ -6,12 +6,76 @@ const { useState, useEffect, useCallback } = SDK.hooks; const { Card, CardHeader, CardTitle, CardContent, Button } = SDK.components; + // Auth-aware fetch: use SDK.fetchJSON if available (handles profile token/headers), + // fallback to raw fetch with ?profile= forwarding async function safeFetchJSON(path, opts) { - const res = await fetch(path, opts || {}); + // Prefer SDK's auth-aware helper + if (SDK && SDK.fetchJSON) { + try { + const data = await SDK.fetchJSON(path, opts || {}); + // SDK.fetchJSON returns parsed JSON directly + return data; + } catch (e) { + // SDK.fetchJSON may throw with body, try to extract + if (e && e.data) return e.data; + // fallback to raw fetch below + if (e && e.message && e.message.includes("Unauthorized")) { + // will try raw with profile below + } else { + throw e; + } + } + } + // Fallback: raw fetch but forward ?profile= from current URL (hermes needs it behind nginx) + let url = path; + try { + const cur = new URL(window.location.href); + const profile = cur.searchParams.get("profile"); + if (profile) { + const u = new URL(path, window.location.origin); + if (!u.searchParams.get("profile")) u.searchParams.set("profile", profile); + url = u.pathname + u.search; + } + } catch (_) { url = path; } + const res = await fetch(url, opts || {}); const text = await res.text(); - try { return JSON.parse(text); } catch (_) { + let json; + try { json = JSON.parse(text); } catch (_) { throw new Error("Server returned non-JSON (status " + res.status + "): " + text.slice(0,800)); } + if (!res.ok) { + // attach body for error display but also return json so UI can show debug + const err = new Error(json.message || json.detail || ("HTTP " + res.status + ": " + text.slice(0,300))); + err.data = json; + err.status = res.status; + // if 401/403, include profile hint + if (res.status === 401) err.message += " (Unauthorized — thiếu profile/token, đang thử lại với ?profile=)"; + throw err; + } + return json; + } + + // Wrapper that returns JSON even on error status so UI can show debug + async function fetchJSONAllowError(path, opts) { + if (SDK && SDK.fetchJSON) { + try { return await SDK.fetchJSON(path, opts || {}); } catch (e) { + if (e && e.data) return e.data; + throw e; + } + } + let url = path; + try { + const cur = new URL(window.location.href); + const profile = cur.searchParams.get("profile"); + if (profile) { + const u = new URL(path, window.location.origin); + if (!u.searchParams.get("profile")) u.searchParams.set("profile", profile); + url = u.pathname + u.search; + } + } catch (_) {} + const res = await fetch(url, opts || {}); + const text = await res.text(); + try { return JSON.parse(text); } catch (_) { return { status: "error", message: text.slice(0,800), http_status: res.status }; } } function SSHXPage() { @@ -21,7 +85,7 @@ const [debug, setDebug] = useState(null); const fetchDebug = useCallback(async function(){ - try { const d = await safeFetchJSON("/api/plugins/sshx-link/debug"); setDebug(d); } catch(_){} + try { const d = await fetchJSONAllowError("/api/plugins/sshx-link/debug"); setDebug(d); } catch(e){ setDebug({error: e.message}); } }, []); const start = useCallback(async function () { @@ -29,7 +93,10 @@ setError(null); setDebug(null); try { - var st = await safeFetchJSON("/api/plugins/sshx-link/status"); + var st = await fetchJSONAllowError("/api/plugins/sshx-link/status"); + if (st && st.detail === "Unauthorized") { + throw new Error("Unauthorized — thử dùng SDK.fetchJSON (đã fix). Hãy hard reload Ctrl+Shift+R sau khi update plugin v2.0.2"); + } if (st.status === "installing" || st.status === "starting") { setStatus("installing"); setError(st.message || "Đang xử lý..."); @@ -41,11 +108,10 @@ setStatus("running"); return; } - if (st.status === "error" && st.message) { - // show previous error but try to restart anyway + var res = await fetchJSONAllowError("/api/plugins/sshx-link/start"); + if (res.detail === "Unauthorized") { + throw new Error("Unauthorized khi gọi /start — plugin frontend chưa dùng SDK auth. Đã fix ở v2.0.2, hãy hard reload."); } - var res = await safeFetchJSON("/api/plugins/sshx-link/start"); - // res may be 202 starting if (res.link) { setLink(res.link); setStatus("running"); @@ -54,13 +120,13 @@ setError(res.message || "sshx đang khởi động, chờ 2s rồi thử lại... " + (res.output_tail || "")); fetchDebug(); } else { - setError((res.message || JSON.stringify(res)) + (res.output_tail ? "\nTail: " + res.output_tail : "") + (res.debug ? "\nDebug: " + JSON.stringify(res.debug) : "")); + setError((res.message || res.detail || JSON.stringify(res)) + (res.output_tail ? "\nTail: " + res.output_tail : "") + (res.debug ? "\nDebug: " + JSON.stringify(res.debug) : "")); setStatus("error"); fetchDebug(); } } catch (err) { - // err may contain JSON body in message - setError(err.message || "Failed to start sshx"); + const detail = err.data ? (err.data.detail || err.data.message || JSON.stringify(err.data)) : ""; + setError((err.message || "Failed to start sshx") + (detail ? " | detail: " + detail : "")); setStatus("error"); fetchDebug(); } @@ -68,7 +134,7 @@ const stop = useCallback(async function () { try { - await safeFetchJSON("/api/plugins/sshx-link/stop", { method: "POST" }); + await fetchJSONAllowError("/api/plugins/sshx-link/stop", { method: "POST" }); setLink(null); setStatus("idle"); setError(null); @@ -80,7 +146,8 @@ const checkStatus = useCallback(async function () { try { - const res = await safeFetchJSON("/api/plugins/sshx-link/status"); + const res = await fetchJSONAllowError("/api/plugins/sshx-link/status"); + if (res.detail === "Unauthorized") return; if (res.status === "running" && res.link) { setLink(res.link); setStatus("running"); @@ -213,7 +280,7 @@ }, status === "loading" ? "Đang tạo shell..." : "Start sshx → Lấy link"), React.createElement(Button, { onClick: fetchDebug, variant:"outline", size:"sm"}, "Debug") ), - React.createElement("p", { className: "text-xs text-muted-foreground", style: { marginTop: "0.75rem"}}, "Nếu lỗi 'No link returned' hoặc timeout, bấm Debug để xem arch/url/binary, kiểm tra server có ra được s3.amazonaws.com và sshx.io không (firewall/security group).") + React.createElement("p", { className: "text-xs text-muted-foreground", style: { marginTop: "0.75rem"}}, "Nếu lỗi Unauthorized, hãy hard reload (Ctrl+Shift+R) để load v2.0.2 — bản này dùng SDK.fetchJSON có auth và tự forward ?profile=.") ) ); } diff --git a/dashboard/manifest.json b/dashboard/manifest.json index 91c7a3a..0d4c50e 100644 --- a/dashboard/manifest.json +++ b/dashboard/manifest.json @@ -3,7 +3,7 @@ "label": "sshx.io Terminal", "description": "Tạo shell ở sshx.io và trả link chia sẻ — auto arch, không cần restart", "icon": "Terminal", - "version": "2.0.1", + "version": "2.0.2", "tab": { "path": "/sshx", "position": "end" diff --git a/plugin.yaml b/plugin.yaml index 7830fa1..f66d65f 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,5 +1,5 @@ name: sshx-link -version: 2.0.1 +version: 2.0.2 description: Tạo shell ở sshx.io và trả link chia sẻ — auto arch detection, không cần restart dashboard author: sisyphus provides_tools: