diff --git a/components/SuperDebugPanel.tsx b/components/SuperDebugPanel.tsx index 6d5c54b..6cfebe5 100644 --- a/components/SuperDebugPanel.tsx +++ b/components/SuperDebugPanel.tsx @@ -236,6 +236,7 @@ export function SuperDebugPanel({ idRef.current = 0; const baseUrl = buildUrl(); + const portNum = port.trim() ? parseInt(port, 10) : undefined; const controller = new AbortController(); abortRef.current = controller; setRunning(true); @@ -312,7 +313,7 @@ export function SuperDebugPanel({ const loginBody = await loginResp.text(); const bodyTrimmed = loginBody.trim(); - if (bodyTrimmed === 'Ok.' || bodyTrimmed === 'Ok') { + if (bodyTrimmed === 'Ok.' || bodyTrimmed === 'Ok' || loginResp.status === 204) { addEntry('LOGIN', `Login successful — "${bodyTrimmed}" (HTTP ${loginResp.status}, ${loginLatency}ms)`, 'success'); } else if (bodyTrimmed === 'Fails.' || bodyTrimmed === 'Fails') { addEntry('LOGIN', `Login REJECTED — "${bodyTrimmed}" (HTTP ${loginResp.status}, ${loginLatency}ms)`, 'error'); @@ -349,9 +350,11 @@ export function SuperDebugPanel({ loginResp.headers.get('SET-COOKIE'); if (setCookieHeader) { - const sidMatch = setCookieHeader.match(/SID=([^;]+)/i); + // regex handles SID with and without the port info + const re = new RegExp(`SID?(?:_${portNum})=([^;]+)`, 'i'); + const sidMatch = setCookieHeader.match(re); if (sidMatch) { - sidCookie = `SID=${sidMatch[1]}`; + sidCookie = `QBT_SID_${portNum}=${sidMatch[1]}`; const truncated = sidMatch[1].length > 12 ? sidMatch[1].substring(0, 12) + '...' : sidMatch[1]; diff --git a/services/api/auth.ts b/services/api/auth.ts index f63e653..ac8da7e 100644 --- a/services/api/auth.ts +++ b/services/api/auth.ts @@ -15,26 +15,25 @@ export const authApi = { // Clear any existing cookies before login apiClient.clearCookies(); clogInfo('AUTH', `Login attempt for user "${username}"`); - - const response = await apiClient.postUrlEncoded(`/api/${API_VERSION}/auth/login`, { + const [response, responseStatus] = await apiClient.postUrlEncoded(`/api/${API_VERSION}/auth/login`, { username, password, }, signal); - + // Check if cookies were set after login const cookies = apiClient.getCookies(); - const responsePreview = typeof response === 'string' ? response.substring(0, 50) : 'non-string response'; console.log('[Auth] Login response:', responsePreview); + console.log('[Auth] Login response status:', responseStatus); console.log('[Auth] Cookies received:', cookies ? 'Yes (' + cookies.length + ' chars)' : 'No'); - clogDebug('AUTH', `Response: "${responsePreview}" | Cookies: ${cookies ? 'Yes (' + cookies.length + ' chars)' : 'No'}`); - + clogDebug('AUTH', `Response [${responseStatus}]: "${responsePreview}" | Cookies: ${cookies ? 'Yes (' + cookies.length + ' chars)' : 'No'}`); + // qBittorrent returns 'Ok.' on success, 'Fails.' on failure + // on newer API versions the response data is empty and success is indicated with a 204 status code // Handle both string and trimmed string responses const responseStr = typeof response === 'string' ? response.trim() : String(response).trim(); - - if (responseStr === 'Ok.' || responseStr === 'Ok') { + if (responseStr === 'Ok.' || responseStr === 'Ok' || responseStatus === 204) { // Successful login - verify we have session cookies if (!cookies || cookies.length === 0) { console.warn('[Auth] Warning: Login succeeded but no cookies received. This may cause issues with qBittorrent 5.x'); @@ -43,7 +42,7 @@ export const authApi = { clogInfo('AUTH', 'Login successful'); return { status: 'Ok' }; } - + console.warn('[Auth] Login failed with response:', responseStr); clogWarn('AUTH', `Login failed — server responded: "${responseStr}"`); return { status: 'Fails' }; @@ -60,15 +59,15 @@ export const authApi = { const statusHint = axiosErr?.response?.status ? ` (HTTP ${axiosErr.response.status})` : ''; console.warn('[Auth] Login error:', message, statusHint); clogError('AUTH', `Login error: ${message}${statusHint}`); - + if (message.includes('timeout') || message.includes('Connection') || message.includes('Network')) { throw error; } - + if (axiosErr?.response?.status === 403) { throw new Error('Authentication failed. Please check your username and password.'); } - + return { status: 'Fails' }; } }, diff --git a/services/api/client.ts b/services/api/client.ts index 859c53f..a07cf09 100644 --- a/services/api/client.ts +++ b/services/api/client.ts @@ -225,7 +225,7 @@ class ApiClient { // Let the interceptor handle headers (it already sets Content-Type) and baseURL const response = await this.client.post(url, body, { signal }); - return response.data; + return [response.data, response.status]; } private isRetriableError(error: unknown): boolean { diff --git a/services/api/torrents.ts b/services/api/torrents.ts index 5a68dc6..510db06 100644 --- a/services/api/torrents.ts +++ b/services/api/torrents.ts @@ -45,11 +45,11 @@ export const torrentsApi = { if (hashes && hashes.length > 0) params.hashes = hashes.join('|'); const response = await apiClient.get(`/api/${API_VERSION}/torrents/info`, params); - + if (Array.isArray(response)) { return response as TorrentInfo[]; } - + return []; }, @@ -108,7 +108,7 @@ export const torrentsApi = { async pauseTorrents(hashes: string[]): Promise { const hashString = hashes.join('|'); try { - const response = await apiClient.postUrlEncoded(`/api/${API_VERSION}/torrents/stop`, { + const [response, responseStatus] = await apiClient.postUrlEncoded(`/api/${API_VERSION}/torrents/stop`, { hashes: hashString, }); // console.log('Pause response:', response); @@ -131,7 +131,7 @@ export const torrentsApi = { async resumeTorrents(hashes: string[]): Promise { const hashString = hashes.join('|'); try { - const response = await apiClient.postUrlEncoded(`/api/${API_VERSION}/torrents/start`, { + const [response, responseStatus] = await apiClient.postUrlEncoded(`/api/${API_VERSION}/torrents/start`, { hashes: hashString, }); } catch (error: unknown) { @@ -199,7 +199,7 @@ export const torrentsApi = { } ): Promise { const formData = new FormData(); - + if (Array.isArray(urls)) { urls.forEach((url) => { formData.append('urls', url); @@ -274,7 +274,7 @@ export const torrentsApi = { } ): Promise { const formData = new FormData(); - + // Add the torrent file // @ts-expect-error React Native FormData accepts { uri, type, name } objects for file uploads formData.append('torrents', {