Merge pull request 'fix: refresh terminal websocket auth on reconnect' (#32) from dev into main
Deploy Dashboard / deploy (push) Successful in 1m13s
Deploy Dashboard / deploy (push) Successful in 1m13s
This commit was merged in pull request #32.
This commit is contained in:
@@ -54,6 +54,15 @@ async function tryRefresh() {
|
||||
return refreshPromise;
|
||||
}
|
||||
|
||||
export async function getWebSocketToken(forceRefresh = false) {
|
||||
if (!forceRefresh && token) return token;
|
||||
const refreshed = await tryRefresh();
|
||||
if (refreshed && token) return token;
|
||||
setToken("");
|
||||
setRefreshToken("");
|
||||
return "";
|
||||
}
|
||||
|
||||
async function request(path, opts = {}) {
|
||||
const headers = opts.headers || {};
|
||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { VoiceSession } from "../lib/voice.js";
|
||||
import { getToken } from "../lib/api.js";
|
||||
import { getWebSocketToken } from "../lib/api.js";
|
||||
|
||||
const PERSISTENCE_STATUS_PREFIX = "__DASHBOARD_PERSISTENCE__:";
|
||||
const PERSISTENCE_STATUS_PREFIX_BYTES = new TextEncoder().encode(PERSISTENCE_STATUS_PREFIX);
|
||||
@@ -189,11 +189,11 @@
|
||||
fit.fit();
|
||||
|
||||
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const token = getToken() || "";
|
||||
let heartbeat = null;
|
||||
let reconnectTimer = null;
|
||||
let closedManually = false;
|
||||
let reconnecting = false;
|
||||
let authRecoveryInFlight = false;
|
||||
|
||||
function clearHeartbeat() {
|
||||
if (heartbeat) {
|
||||
@@ -202,6 +202,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleAuthExpired(reason = "Session expired — please log in again") {
|
||||
clearHeartbeat();
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
}
|
||||
reconnecting = false;
|
||||
updateTab(tabId, {
|
||||
connected: false,
|
||||
error: reason,
|
||||
statusBanner: "",
|
||||
});
|
||||
term.write(`\r\n\x1b[90m[${reason}]\x1b[0m`);
|
||||
const d = tabData.get(tabId);
|
||||
if (d) {
|
||||
d.ws = null;
|
||||
d.heartbeat = heartbeat;
|
||||
d.reconnectTimer = reconnectTimer;
|
||||
d.closedManually = closedManually;
|
||||
d.reconnecting = reconnecting;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleReconnect(reason) {
|
||||
clearHeartbeat();
|
||||
if (closedManually || reconnectTimer) return;
|
||||
@@ -215,7 +238,7 @@
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectTimer = null;
|
||||
if (!tabData.has(tabId) || closedManually) return;
|
||||
connect();
|
||||
connect(true);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
@@ -229,8 +252,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
function connect() {
|
||||
const token = getToken() || "";
|
||||
async function connect(forceRefresh = false) {
|
||||
const token = await getWebSocketToken(forceRefresh);
|
||||
if (!token) {
|
||||
handleAuthExpired();
|
||||
return null;
|
||||
}
|
||||
|
||||
const ws = new WebSocket(
|
||||
`${proto}//${location.host}/ws/terminal?host=${encodeURIComponent(tab.hostId)}&token=${encodeURIComponent(token)}`
|
||||
);
|
||||
@@ -272,10 +300,30 @@
|
||||
}
|
||||
term.write(data);
|
||||
};
|
||||
ws.onclose = (e) => {
|
||||
ws.onclose = async (e) => {
|
||||
const reason = e.reason || `Connection closed (code ${e.code})`;
|
||||
const d = tabData.get(tabId);
|
||||
if (d) d.ws = null;
|
||||
if (e.code === 1008 && reason === "Unauthorized") {
|
||||
if (!closedManually && !authRecoveryInFlight) {
|
||||
authRecoveryInFlight = true;
|
||||
const recoveredToken = await getWebSocketToken(true);
|
||||
authRecoveryInFlight = false;
|
||||
if (recoveredToken) {
|
||||
reconnecting = true;
|
||||
updateTab(tabId, {
|
||||
connected: false,
|
||||
error: "Refreshing session...",
|
||||
statusBanner: tab.persistent ? "Reconnecting to persistent session..." : "",
|
||||
});
|
||||
term.write(`\r\n\x1b[90m[Refreshing session...]\x1b[0m`);
|
||||
void connect(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
handleAuthExpired("Session expired — please log in again");
|
||||
return;
|
||||
}
|
||||
if (!closedManually) scheduleReconnect(reason);
|
||||
else {
|
||||
clearHeartbeat();
|
||||
@@ -301,7 +349,11 @@
|
||||
return ws;
|
||||
}
|
||||
|
||||
let ws = connect();
|
||||
let ws = null;
|
||||
void connect().then((connectedWs) => {
|
||||
ws = connectedWs;
|
||||
if (connectedWs) voice.attach(term, connectedWs);
|
||||
});
|
||||
term.onData((data) => {
|
||||
const currentWs = tabData.get(tabId)?.ws;
|
||||
if (currentWs?.readyState === 1) currentWs.send(new TextEncoder().encode(data));
|
||||
|
||||
Reference in New Issue
Block a user