This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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__}`;
|
||||
|
||||
|
||||
@@ -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*</);
|
||||
});
|
||||
Reference in New Issue
Block a user