diff --git a/src/main/classes/controllers/AccountController.ts b/src/main/classes/controllers/AccountController.ts index 18a5a45e..9db2cf83 100644 --- a/src/main/classes/controllers/AccountController.ts +++ b/src/main/classes/controllers/AccountController.ts @@ -127,13 +127,9 @@ export class AccountController { try { // Make a simple API call to verify the token is still accepted by the server - // Use the /api/user/me endpoint to validate the token - const testUrl = `https://${lastLoggedAccount.host}/api/user/me` - const response = await NetworkController.instance.get(testUrl, { - headers: { - 'Authorization': `Bearer ${lastLoggedAccount.jwtToken}` - } - } as any) + // Use the API client so path selection (/api vs /webrest) follows account settings/fallback logic + const { NethVoiceAPI } = useNethVoiceAPI(lastLoggedAccount) + await NethVoiceAPI.User.me() // If we get here, the token is valid on the server Log.info('auto login: token validated with server, using saved token') diff --git a/src/renderer/src/hooks/usePhoneIsland.ts b/src/renderer/src/hooks/usePhoneIsland.ts index b0e9b236..86ee6254 100644 --- a/src/renderer/src/hooks/usePhoneIsland.ts +++ b/src/renderer/src/hooks/usePhoneIsland.ts @@ -8,14 +8,19 @@ export const usePhoneIsland = () => { const { NethVoiceAPI } = useLoggedNethVoiceAPI() const createDataConfig = async (account: Account): Promise<[Extension, string]> => { - const phoneIslandTokenLoginResponse = (await NethVoiceAPI.Authentication.phoneIslandTokenLogin()).token + const tokenResponse = await NethVoiceAPI.Authentication.phoneIslandTokenLogin() + const phoneIslandToken = tokenResponse?.token + if (!phoneIslandToken) { + throw new Error('Unable to retrieve dedicated Phone Island token') + } + const deviceInformationObject: Extension | undefined = account.data!.endpoints.extension.find((e) => e.type === 'nethlink') if (deviceInformationObject) { const hostname = account!.host const config: PhoneIslandConfig = { hostname, username: account.username, - authToken: phoneIslandTokenLoginResponse, + authToken: phoneIslandToken, sipExten: deviceInformationObject.id, sipSecret: deviceInformationObject.secret, sipHost: account.sipHost || '', diff --git a/src/shared/useNethVoiceAPI.ts b/src/shared/useNethVoiceAPI.ts index 228e25d9..e0f4bb26 100644 --- a/src/shared/useNethVoiceAPI.ts +++ b/src/shared/useNethVoiceAPI.ts @@ -24,7 +24,7 @@ const PRIMARY_API_BASE_PATH = '/api' const FALLBACK_API_BASE_PATH = '/webrest' export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) => { - const { GET, POST } = useNetwork() + const { GET, POST, DELETE } = useNetwork() let isFirstHeartbeat = true let account: Account | undefined = loggedAccount || undefined // Use account's stored API path preference, or default to primary @@ -46,9 +46,6 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) if (endpoint === '/logout') { return `${FALLBACK_API_BASE_PATH}/authentication/logout` } - if (endpoint === '/authentication/phone_island_token_login') { - return `${FALLBACK_API_BASE_PATH}/authentication/phone_island_token_login` - } // For other endpoints, use webrest format return `${FALLBACK_API_BASE_PATH}${endpoint}` } @@ -147,6 +144,17 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) } } + async function _DELETE(path: string, hasAuth = true): Promise { + try { + return (await DELETE(_joinUrl(path), _getHeaders(hasAuth))) + } catch (e) { + if (!path.includes('login') && !path.includes('2fa/verify-otp')) { + console.error(e) + } + throw e + } + } + function shouldTryFallback(path: string, error: any): boolean { // Only try fallback if we're using primary path if (currentApiBasePath !== PRIMARY_API_BASE_PATH) { @@ -393,11 +401,22 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) }) }, - phoneIslandTokenLogin: async (): Promise<{ username: string, token: string }> => - await _POST(buildApiPath('/authentication/phone_island_token_login'), { subtype: 'nethlink' }), + // Dedicated token for Phone Island in NethLink (kept separate by design). + phoneIslandTokenLogin: async (): Promise<{ username: string, token: string }> => { + if (currentApiBasePath === FALLBACK_API_BASE_PATH) { + return await _POST(buildApiPath('/authentication/phone_island_token_login'), { subtype: 'nethlink' }) + } + + return await POST(_joinUrl(buildApiPath('/tokens/persistent/nethlink')), undefined, _getHeaders()) + }, - phoneIslandTokenLogout: async (): Promise<{ username: string, token: string }> => - await _POST(buildApiPath('/authentication/persistent_token_remove'), { type: 'phone-island', subtype: 'nethlink' }), + phoneIslandTokenLogout: async (): Promise => { + if (currentApiBasePath === FALLBACK_API_BASE_PATH) { + return await _POST(buildApiPath('/authentication/persistent_token_remove'), { type: 'phone-island', subtype: 'nethlink' }) + } + + return await DELETE(_joinUrl(buildApiPath('/tokens/persistent/nethlink')), _getHeaders()) + }, } const CustCard = {} diff --git a/src/shared/useNetwork.ts b/src/shared/useNetwork.ts index 79f20142..f55ca20d 100644 --- a/src/shared/useNetwork.ts +++ b/src/shared/useNetwork.ts @@ -27,6 +27,17 @@ export const useNetwork = () => { } } + async function DELETE(path: string, config: { headers: { Authorization?: string | undefined; 'Content-Type': string } } | undefined = { headers: { 'Content-Type': 'application/json' } }): Promise { + try { + const response = await axios.delete(path, config) + return response.data + } catch (e: any) { + const err: AxiosError = e + Log.error('during fetch DELETE', err.name, err.code, err.message, path, config) + throw e + } + } + async function HEAD(path: string, timeoutMs: number = 5000): Promise { try { await axios.head(path, { @@ -43,6 +54,7 @@ export const useNetwork = () => { return { GET, POST, + DELETE, HEAD } }