fix: lock IM tenant to default
This commit is contained in:
@@ -4,7 +4,7 @@ import { Link, Navigate, useNavigate } from 'react-router-dom';
|
|||||||
|
|
||||||
import { Icon } from '@/app/Icon';
|
import { Icon } from '@/app/Icon';
|
||||||
import { PasswordInput } from '@/features/auth/components/PasswordInput';
|
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 { getTinode, loginBasic } from '@/lib/tinode/connection';
|
||||||
import { loginErrorMessage, validateCredentialsMessage } from '@/lib/tinode/errors';
|
import { loginErrorMessage, validateCredentialsMessage } from '@/lib/tinode/errors';
|
||||||
import { useSessionStore } from '@/stores/session';
|
import { useSessionStore } from '@/stores/session';
|
||||||
@@ -25,7 +25,7 @@ export function LoginPage() {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
if (!tenant) {
|
if (!tenant || tenant.code !== FIXED_TENANT_CODE) {
|
||||||
// Read tenant before touching getTinode() -- getTinode() lazily creates and PERMANENTLY caches
|
// 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
|
// 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
|
// 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>
|
||||||
|
|
||||||
<div className="flex flex-1 flex-col bg-white px-6 py-8">
|
<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">
|
<div className="text-sm text-text-secondary">企业码:{FIXED_TENANT_CODE}</div>
|
||||||
<Icon name="arrow_back_ios_new" className="text-[16px]" />
|
|
||||||
切换企业
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className="flex flex-1 flex-col items-center justify-center">
|
<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">
|
<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 { Icon } from '@/app/Icon';
|
||||||
import { ImApiError } from '@/lib/api/client';
|
import { ImApiError } from '@/lib/api/client';
|
||||||
import { resolveTenant } from '@/lib/api/tenant';
|
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 { resetTinode } from '@/lib/tinode/connection';
|
||||||
import { requestConfirm } from '@/stores/confirmDialog';
|
import { requestConfirm } from '@/stores/confirmDialog';
|
||||||
import { parseWebsocketEndpoint, useSessionStore } from '@/stores/session';
|
import { parseWebsocketEndpoint, useSessionStore } from '@/stores/session';
|
||||||
@@ -17,13 +17,12 @@ export function TenantEntryPage() {
|
|||||||
const setTenant = useSessionStore((s) => s.setTenant);
|
const setTenant = useSessionStore((s) => s.setTenant);
|
||||||
const setMyUserId = useSessionStore((s) => s.setMyUserId);
|
const setMyUserId = useSessionStore((s) => s.setMyUserId);
|
||||||
const myUserId = useSessionStore((s) => s.myUserId);
|
const myUserId = useSessionStore((s) => s.myUserId);
|
||||||
const [code, setCode] = useState('');
|
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
const submit = async (e: React.FormEvent) => {
|
const submit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!code.trim() || submitting) return;
|
if (submitting) return;
|
||||||
// Reachable by navigating back here while already logged in (no route guard keeps an
|
// 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
|
// 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
|
// 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
|
// 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.
|
// never mounts for this route. Checking the token directly closes that gap.
|
||||||
const hasLiveSession = myUserId || sessionStorage.getItem('im-session-token');
|
const hasLiveSession = myUserId || sessionStorage.getItem('im-session-token');
|
||||||
if (hasLiveSession && !(await requestConfirm({ message: '切换企业将退出当前登录,确定继续吗?' }))) return;
|
if (hasLiveSession && !(await requestConfirm({ message: '重新进入将退出当前登录,确定继续吗?' }))) return;
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const res = await resolveTenant(code.trim());
|
const res = await resolveTenant(FIXED_TENANT_CODE);
|
||||||
const { host, secure } = parseWebsocketEndpoint(res.endpoints.websocket);
|
const { host, secure } = parseWebsocketEndpoint(res.endpoints.websocket);
|
||||||
// Without this, switching tenants mid-session silently keeps talking to the OLD tenant's
|
// 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
|
// host: getTinode() lazily creates one singleton and reuses it forever once created, and
|
||||||
@@ -73,7 +72,7 @@ export function TenantEntryPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-dvh w-full items-center justify-center bg-surface-page px-6">
|
<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>
|
</Link>
|
||||||
<form onSubmit={submit} className="flex w-full max-w-[380px] flex-col gap-5">
|
<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">
|
<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" />
|
<Icon name="business" className="text-[20px] text-focus-border" />
|
||||||
<input
|
<input
|
||||||
value={code}
|
value={FIXED_TENANT_CODE}
|
||||||
onChange={(e) => setCode(e.target.value)}
|
readOnly
|
||||||
placeholder="请输入企业码"
|
aria-label="企业码"
|
||||||
maxLength={64}
|
className="w-full cursor-not-allowed bg-transparent text-[15px] text-text-secondary outline-none"
|
||||||
className="w-full bg-transparent text-[15px] outline-none"
|
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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"
|
className="h-[50px] rounded-lg bg-btn text-base font-medium text-white shadow-btn disabled:bg-btn-disabled disabled:shadow-none"
|
||||||
>
|
>
|
||||||
{submitting ? '进入中…' : '进入企业'}
|
{submitting ? '进入中…' : '进入企业'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</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>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
/** User-visible product name. */
|
/** User-visible product name. */
|
||||||
export const BASE_APP_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. */
|
/** Name used in the Tinode client's User-Agent string. */
|
||||||
export const APP_NAME = `MeetingIM/${__APP_VERSION__}`;
|
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