Merge dev to main - Immich and Terminal fixes #36

Merged
jimmy merged 5 commits from dev into main 2026-03-15 22:50:19 +08:00
2 changed files with 42 additions and 8 deletions
Showing only changes of commit 7b17a53cab - Show all commits
+14 -3
View File
@@ -2,6 +2,7 @@ import asyncio
import struct import struct
import logging import logging
import shlex import shlex
import jwt
from collections import Counter from collections import Counter
from fastapi import WebSocket, Query from fastapi import WebSocket, Query
import asyncssh import asyncssh
@@ -90,19 +91,29 @@ async def _release_session(session_id: int | None):
_active_sessions.pop(session_id, None) _active_sessions.pop(session_id, None)
async def ws_endpoint(websocket: WebSocket, host: str = Query("nas"), token: str | None = Query(None)): async def ws_endpoint(websocket: WebSocket, host: str = Query("nas")):
if host not in HOSTS: if host not in HOSTS:
await websocket.close(code=1008, reason="Unknown host") await websocket.close(code=1008, reason="Unknown host")
return return
access_token = websocket.cookies.get(auth.ACCESS_COOKIE_NAME) or token access_token = websocket.cookies.get(auth.ACCESS_COOKIE_NAME)
if not access_token: if not access_token:
logger.warning("WebSocket auth failed for %s: no access token cookie", host)
await websocket.close(code=1008, reason="Unauthorized") await websocket.close(code=1008, reason="Unauthorized")
return return
try: try:
user = await auth.get_current_user_ws(access_token) user = await auth.get_current_user_ws(access_token)
except Exception: except jwt.ExpiredSignatureError as e:
logger.warning("WebSocket auth failed for %s: token expired - %s", host, e)
await websocket.close(code=1008, reason="Token expired")
return
except jwt.InvalidTokenError as e:
logger.warning("WebSocket auth failed for %s: invalid token - %s", host, e)
await websocket.close(code=1008, reason="Invalid token")
return
except Exception as e:
logger.error("WebSocket auth failed for %s: unexpected error - %s", host, e)
await websocket.close(code=1008, reason="Unauthorized") await websocket.close(code=1008, reason="Unauthorized")
return return
+28 -5
View File
@@ -196,8 +196,11 @@
let reconnecting = false; let reconnecting = false;
let authRecoveryInFlight = false; let authRecoveryInFlight = false;
let reconnectAttempts = 0; let reconnectAttempts = 0;
let consecutiveAuthFailures = 0;
const MAX_RECONNECT_DELAY = 30000; const MAX_RECONNECT_DELAY = 30000;
const PONG_TIMEOUT = 5000; // 5s to receive pong const MAX_AUTH_FAILURES = 3;
const PING_INTERVAL = 30000; // 30s between pings
const PONG_TIMEOUT = 10000; // 10s to receive pong
function clearHeartbeat() { function clearHeartbeat() {
if (heartbeat) { if (heartbeat) {
@@ -263,12 +266,18 @@
} }
async function connect(forceRefresh = false) { async function connect(forceRefresh = false) {
if (forceRefresh) { // Always refresh token before first connection to ensure fresh cookies
const d = tabData.get(tabId);
const isFirstConnect = !d?.hasConnectedOnce;
if (forceRefresh || isFirstConnect) {
const refreshed = await tryRefreshSession(); const refreshed = await tryRefreshSession();
if (!refreshed) { if (!refreshed) {
handleAuthExpired(); handleAuthExpired();
return null; return null;
} }
// Mark that we've connected once with fresh token
if (d) d.hasConnectedOnce = true;
} }
const ws = new WebSocket( const ws = new WebSocket(
@@ -278,6 +287,7 @@
ws.onopen = () => { ws.onopen = () => {
reconnectAttempts = 0; reconnectAttempts = 0;
consecutiveAuthFailures = 0; // Reset auth failure counter on successful connection
updateTab(tabId, { updateTab(tabId, {
connected: true, connected: true,
error: "", error: "",
@@ -295,7 +305,7 @@
} }
}, PONG_TIMEOUT); }, PONG_TIMEOUT);
} }
}, 12000); }, PING_INTERVAL);
const d = tabData.get(tabId); const d = tabData.get(tabId);
if (d) { if (d) {
d.ws = ws; d.ws = ws;
@@ -335,7 +345,18 @@
const reason = e.reason || `Connection closed (code ${e.code})`; const reason = e.reason || `Connection closed (code ${e.code})`;
const d = tabData.get(tabId); const d = tabData.get(tabId);
if (d) d.ws = null; if (d) d.ws = null;
if (e.code === 1008 && reason === "Unauthorized") {
// Handle auth failures (code 1008)
if (e.code === 1008) {
consecutiveAuthFailures++;
// Stop after MAX_AUTH_FAILURES consecutive auth failures
if (consecutiveAuthFailures >= MAX_AUTH_FAILURES) {
handleAuthExpired(`Session expired after ${MAX_AUTH_FAILURES} attempts — please refresh page`);
return;
}
// Try to refresh session
if (!closedManually && !authRecoveryInFlight) { if (!closedManually && !authRecoveryInFlight) {
authRecoveryInFlight = true; authRecoveryInFlight = true;
const refreshed = await tryRefreshSession(); const refreshed = await tryRefreshSession();
@@ -344,7 +365,7 @@
reconnecting = true; reconnecting = true;
updateTab(tabId, { updateTab(tabId, {
connected: false, connected: false,
error: "Refreshing session...", error: `Refreshing session (attempt ${consecutiveAuthFailures}/${MAX_AUTH_FAILURES})...`,
statusBanner: tab.persistent ? "Reconnecting to persistent session..." : "", statusBanner: tab.persistent ? "Reconnecting to persistent session..." : "",
}); });
term.write(`\r\n\x1b[90m[Refreshing session...]\x1b[0m`); term.write(`\r\n\x1b[90m[Refreshing session...]\x1b[0m`);
@@ -355,6 +376,8 @@
handleAuthExpired("Session expired — please log in again"); handleAuthExpired("Session expired — please log in again");
return; return;
} }
// Non-auth failures: continue with normal reconnection
if (!closedManually) scheduleReconnect(reason); if (!closedManually) scheduleReconnect(reason);
else { else {
clearHeartbeat(); clearHeartbeat();