fix: lock IM tenant to default

This commit is contained in:
richjohnrambo
2026-10-19 11:28:07 +08:00
parent 72c4f4e54d
commit c063d056b2
4 changed files with 41 additions and 19 deletions
+3 -6
View File
@@ -4,7 +4,7 @@ import { Link, Navigate, useNavigate } from 'react-router-dom';
import { Icon } from '@/app/Icon';
import { PasswordInput } from '@/features/auth/components/PasswordInput';
import { BASE_APP_NAME } from '@/lib/config';
import { BASE_APP_NAME, FIXED_TENANT_CODE } from '@/lib/config';
import { getTinode, loginBasic } from '@/lib/tinode/connection';
import { loginErrorMessage, validateCredentialsMessage } from '@/lib/tinode/errors';
import { useSessionStore } from '@/stores/session';
@@ -25,7 +25,7 @@ export function LoginPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
if (!tenant) {
if (!tenant || tenant.code !== FIXED_TENANT_CODE) {
// Read tenant before touching getTinode() -- getTinode() lazily creates and PERMANENTLY caches
// the singleton off whatever tenant is in the store at that moment (or a hardcoded fallback
// host if there's none yet); calling it here first would risk creating that singleton against
@@ -73,10 +73,7 @@ export function LoginPage() {
</div>
<div className="flex flex-1 flex-col bg-white px-6 py-8">
<Link to="/tenant" className="flex items-center gap-1 text-sm text-text-secondary no-underline">
<Icon name="arrow_back_ios_new" className="text-[16px]" />
切换企业
</Link>
<div className="text-sm text-text-secondary">企业码:{FIXED_TENANT_CODE}</div>
<div className="flex flex-1 flex-col items-center justify-center">
<form onSubmit={submit} className="flex w-full max-w-[380px] flex-col gap-4">
+11 -13
View File
@@ -6,7 +6,7 @@ import { Link, useNavigate } from 'react-router-dom';
import { Icon } from '@/app/Icon';
import { ImApiError } from '@/lib/api/client';
import { resolveTenant } from '@/lib/api/tenant';
import { BASE_APP_NAME } from '@/lib/config';
import { BASE_APP_NAME, FIXED_TENANT_CODE } from '@/lib/config';
import { resetTinode } from '@/lib/tinode/connection';
import { requestConfirm } from '@/stores/confirmDialog';
import { parseWebsocketEndpoint, useSessionStore } from '@/stores/session';
@@ -17,13 +17,12 @@ export function TenantEntryPage() {
const setTenant = useSessionStore((s) => s.setTenant);
const setMyUserId = useSessionStore((s) => s.setMyUserId);
const myUserId = useSessionStore((s) => s.myUserId);
const [code, setCode] = useState('');
const [error, setError] = useState('');
const [submitting, setSubmitting] = useState(false);
const submit = async (e: React.FormEvent) => {
e.preventDefault();
if (!code.trim() || submitting) return;
if (submitting) return;
// Reachable by navigating back here while already logged in (no route guard keeps an
// authenticated user out of /tenant) -- resetTinode() below would otherwise silently tear down
// that live session with no warning at all. `myUserId` alone isn't enough: it's an in-memory
@@ -33,11 +32,11 @@ export function TenantEntryPage() {
// RequireAuth.tsx, which is the ONLY place that re-populates `myUserId` from that token, and it
// never mounts for this route. Checking the token directly closes that gap.
const hasLiveSession = myUserId || sessionStorage.getItem('im-session-token');
if (hasLiveSession && !(await requestConfirm({ message: '切换企业将退出当前登录,确定继续吗?' }))) return;
if (hasLiveSession && !(await requestConfirm({ message: '重新进入将退出当前登录,确定继续吗?' }))) return;
setSubmitting(true);
setError('');
try {
const res = await resolveTenant(code.trim());
const res = await resolveTenant(FIXED_TENANT_CODE);
const { host, secure } = parseWebsocketEndpoint(res.endpoints.websocket);
// Without this, switching tenants mid-session silently keeps talking to the OLD tenant's
// host: getTinode() lazily creates one singleton and reuses it forever once created, and
@@ -73,7 +72,7 @@ export function TenantEntryPage() {
return (
<div className="relative flex h-dvh w-full items-center justify-center bg-surface-page px-6">
<Link to="/enterprise-register" state={{ tenantCodeHint: code.trim() }} className="absolute right-6 top-6 text-sm text-accent no-underline">
<Link to="/enterprise-register" state={{ tenantCodeHint: FIXED_TENANT_CODE }} className="absolute right-6 top-6 text-sm text-accent no-underline">
企业注册
</Link>
<form onSubmit={submit} className="flex w-full max-w-[380px] flex-col gap-5">
@@ -90,22 +89,21 @@ export function TenantEntryPage() {
<label className="flex h-[50px] items-center gap-2 rounded-lg bg-surface-input px-3.5 has-focus:border has-focus:border-focus-border has-focus:shadow-focus-ring">
<Icon name="business" className="text-[20px] text-focus-border" />
<input
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="请输入企业码"
maxLength={64}
className="w-full bg-transparent text-[15px] outline-none"
value={FIXED_TENANT_CODE}
readOnly
aria-label="企业码"
className="w-full cursor-not-allowed bg-transparent text-[15px] text-text-secondary outline-none"
/>
</label>
<button
type="submit"
disabled={!code.trim() || submitting}
disabled={submitting}
className="h-[50px] rounded-lg bg-btn text-base font-medium text-white shadow-btn disabled:bg-btn-disabled disabled:shadow-none"
>
{submitting ? '进入中…' : '进入企业'}
</button>
</form>
<Link to="/feedback" state={{ tenantCodeHint: code.trim() }} className="absolute bottom-8 text-sm text-accent no-underline">
<Link to="/feedback" state={{ tenantCodeHint: FIXED_TENANT_CODE }} className="absolute bottom-8 text-sm text-accent no-underline">
遇到问题?
</Link>
</div>
+3
View File
@@ -5,6 +5,9 @@
/** User-visible product name. */
export const BASE_APP_NAME = '迅工捷使';
/** This deployment serves a single enterprise; users cannot switch tenants. */
export const FIXED_TENANT_CODE = 'default';
/** Name used in the Tinode client's User-Agent string. */
export const APP_NAME = `MeetingIM/${__APP_VERSION__}`;
+24
View File
@@ -0,0 +1,24 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
const source = (path) => readFile(new URL(`../${path}`, import.meta.url), 'utf8');
test('tenant entry is permanently fixed to default', async () => {
const config = await source('src/lib/config.ts');
const entry = await source('src/features/auth/TenantEntryPage.tsx');
assert.match(config, /FIXED_TENANT_CODE = 'default'/);
assert.match(entry, /value=\{FIXED_TENANT_CODE\}/);
assert.match(entry, /readOnly/);
assert.match(entry, /resolveTenant\(FIXED_TENANT_CODE\)/);
assert.doesNotMatch(entry, /setCode/);
});
test('login page exposes no tenant switch and rejects stale tenant state', async () => {
const login = await source('src/features/auth/LoginPage.tsx');
assert.match(login, /tenant\.code !== FIXED_TENANT_CODE/);
assert.match(login, /企业码:\{FIXED_TENANT_CODE\}/);
assert.doesNotMatch(login, />\s*切换企业\s*</);
});