diff --git a/web/src/features/auth/LoginPage.tsx b/web/src/features/auth/LoginPage.tsx index 034da3c6..82aa3846 100644 --- a/web/src/features/auth/LoginPage.tsx +++ b/web/src/features/auth/LoginPage.tsx @@ -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() {
- - - 切换企业 - +
企业码:{FIXED_TENANT_CODE}
diff --git a/web/src/features/auth/TenantEntryPage.tsx b/web/src/features/auth/TenantEntryPage.tsx index 0dc2a4a5..51978773 100644 --- a/web/src/features/auth/TenantEntryPage.tsx +++ b/web/src/features/auth/TenantEntryPage.tsx @@ -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 (
- + 企业注册 @@ -90,22 +89,21 @@ export function TenantEntryPage() { - + 遇到问题?
diff --git a/web/src/lib/config.ts b/web/src/lib/config.ts index 0d0d650f..077b0594 100644 --- a/web/src/lib/config.ts +++ b/web/src/lib/config.ts @@ -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__}`; diff --git a/web/tests/fixed-tenant.test.mjs b/web/tests/fixed-tenant.test.mjs new file mode 100644 index 00000000..581d3578 --- /dev/null +++ b/web/tests/fixed-tenant.test.mjs @@ -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*