import fetchHelper from './fetchHelper' import { cooldownKey, assertNotCoolingDown, recordFailure, recordSuccess } from './authCooldown' // Treat missing cookies and literal junk values some older flows wrote // ('undefined'/'null'/'') as absent. export const hasCtxValue = (value: any): boolean => value !== undefined && value !== null && value !== '' && value !== 'undefined' && value !== 'null' const decodeCredential = (value: any): string => { if (!hasCtxValue(value)) return '' try { return Buffer.from(String(value), 'base64').toString('utf8') } catch { return '' } } /** * Two-tier token refresh (auth/refresh → auth/tokens with stored credentials) * WITHOUT any cookie/sqlite side effects, so it is safe to call after response * headers are already sent (SSE polling). * * Throws WITHOUT calling iDempiere when the stored cookies cannot possibly * authenticate — empty/missing credentials or incomplete client/role/org * context used to flood AuthFailure.log with empty-username logins * ("Invalid User ID or Password" / "Missing organizationId parameter"). * Failed credential logins are additionally rate-limited per session * (see authCooldown.ts). * * Returns the raw iDempiere token response ({ token, refresh_token, userId, … }). */ export async function refreshTokensRaw(event: any): Promise { const token = getCookie(event, 'logship_it') const refreshToken = getCookie(event, 'logship_rt') const userId = getCookie(event, 'logship_user_id') const clientId = getCookie(event, 'logship_client_id') const roleId = getCookie(event, 'logship_role_id') const organizationId = getCookie(event, 'logship_organization_id') const warehouseId = getCookie(event, 'logship_warehouse_id') const language = getCookie(event, 'logship_language') const userName = decodeCredential(getCookie(event, 'logship_xu')) const password = decodeCredential(getCookie(event, 'logship_py')) const canRefresh = hasCtxValue(refreshToken) && hasCtxValue(userId) && hasCtxValue(clientId) // iDempiere's one-step login rejects clientId+roleId without organizationId // ("Missing organizationId parameter"). organizationId '0' (org "*") is valid. const canCredentialLogin = userName.trim() !== '' && password !== '' && hasCtxValue(clientId) && hasCtxValue(roleId) && hasCtxValue(organizationId) let resToken: any = null let refreshFailed = !canRefresh if (canRefresh) { try { resToken = await fetchHelper(event, 'auth/refresh', 'POST', token, { refresh_token: refreshToken, clientId: clientId, userId: userId }) } catch { resToken = null refreshFailed = true } } // Credential fallback only when the refresh tier failed/was impossible — // mirrors the original try/catch semantics. if (refreshFailed) { if (!canCredentialLogin) { throw createError({ statusCode: 401, statusMessage: 'Session expired', data: { status: 401, message: 'Token refresh not possible: no valid refresh token and no complete stored credentials/context — re-login required' } }) } const key = cooldownKey(event) assertNotCoolingDown(key) const parameters: any = { clientId: clientId, roleId: roleId, organizationId: organizationId, language: hasCtxValue(language) ? language : 'en_US' } // Omit optional keys entirely when unset — never send ''. if (hasCtxValue(warehouseId)) parameters.warehouseId = warehouseId try { resToken = await fetchHelper(event, 'auth/tokens', 'POST', '', { userName: userName, password: password, parameters: parameters }) recordSuccess(key) } catch (error: any) { recordFailure(key) throw error } } return resToken ?? {} } export default refreshTokensRaw