diff --git a/web/src/lib/tinode/connection.ts b/web/src/lib/tinode/connection.ts index 33870422..675caa10 100644 --- a/web/src/lib/tinode/connection.ts +++ b/web/src/lib/tinode/connection.ts @@ -150,6 +150,16 @@ export function clearKickedInfo() { * logins (restoreSession/reconnect re-auth) are untouched -- validateLoginTenant only requires * login_type when scheme is "basic". */ +/** The tenant the server session actually resolves to, captured when {hi} is sent (the server + * falls back to 'default' when {hi}.tenant is absent). login_type is decided against THIS value, + * not a fresh store read: the two decisions happen at different moments (hi on connect, login on + * submit), and a store change in between would make login_type disagree with the session tenant, + * which the server rejects with 422 policy violation / TENANT_CODE_FORBIDDEN -- rendered by + * clients as "当前操作不符合安全策略要求". */ +let sessionTenantCode: string | null = null; +/** One-shot override used by loginBasic's FORBIDDEN self-heal retry (see there). */ +let forcedLoginType: 0 | 1 | null = null; + function installOutgoingPacketPatches(t: Tinode) { const conn = t._connection; if (!conn || conn.__tenantPatched) return; @@ -160,10 +170,13 @@ function installOutgoingPacketPatches(t: Tinode) { const parsed = JSON.parse(msg); if (parsed.hi) { const tenant = useSessionStore.getState().tenant; + // Whichever tenant (or absence of one) this {hi} declares, the server fixes the session + // tenant from it right now -- so remember exactly that, for login_type decisions later. + sessionTenantCode = (tenant?.code || 'default').trim().toLowerCase(); if (tenant?.code) { parsed.hi.tenant = tenant.code; - msg = JSON.stringify(parsed); } + msg = JSON.stringify(parsed); } } catch { // Not valid JSON -- send as is. @@ -172,8 +185,12 @@ function installOutgoingPacketPatches(t: Tinode) { try { const parsed = JSON.parse(msg); if (parsed.login && parsed.login.scheme === 'basic') { - const tenantCode = useSessionStore.getState().tenant?.code || 'default'; - parsed.login.login_type = tenantCode.trim().toLowerCase() === 'default' ? 1 : 0; + if (forcedLoginType !== null) { + parsed.login.login_type = forcedLoginType; + } else { + const tenantCode = sessionTenantCode ?? (useSessionStore.getState().tenant?.code || 'default'); + parsed.login.login_type = tenantCode.trim().toLowerCase() === 'default' ? 1 : 0; + } msg = JSON.stringify(parsed); } } catch { @@ -292,6 +309,11 @@ export function resetTinode() { connectPromise = null; restorePromise = null; wasAuthenticated = false; + // Both belong to the torn-down connection's session: the next getTinode() re-derives them from + // the freshly-sent {hi}, and a stale non-null sessionTenantCode here would misclassify the very + // first login_type of the rebuilt connection (which sends hi before any store read matters). + sessionTenantCode = null; + forcedLoginType = null; setStatus('disconnected'); } @@ -354,7 +376,24 @@ export function loginBasic(login: string, password: string): Promise loginPromise = (async () => { const t = getTinode(); if (!t.isConnected()) await connect(); - return t.loginBasic(login, password); + const ctrl = await t.loginBasic(login, password); + // Self-heal the exact failure mode observed live (server validateLoginTenant): an enterprise- + // meaning login_type=0 against a session whose tenant is 'default' is rejected with 422 policy + // violation, reason=TENANT_CODE_FORBIDDEN -- users see "当前操作不符合安全策略要求". That can + // only mean the session tenant is 'default', so retry once with the personal meaning (1) before + // giving up; a success also re-syncs sessionTenantCode so later logins on this connection don't + // repeat the mistake. + if (ctrl.code === 422 && (ctrl.params as { reason?: string } | undefined)?.reason === 'TENANT_CODE_FORBIDDEN') { + forcedLoginType = 1; + try { + const retry = await t.loginBasic(login, password); + if (retry.code < 300) sessionTenantCode = 'default'; + return retry; + } finally { + forcedLoginType = null; + } + } + return ctrl; })().finally(() => { loginPromise = null; });