feat(web): add tenant user registration flow
This commit is contained in:
@@ -10,6 +10,7 @@ import { RequireAuth } from '@/app/RequireAuth';
|
|||||||
import { SecurityChallengeModal } from '@/app/SecurityChallengeModal';
|
import { SecurityChallengeModal } from '@/app/SecurityChallengeModal';
|
||||||
import { EnterpriseRegisterPage } from '@/features/auth/EnterpriseRegisterPage';
|
import { EnterpriseRegisterPage } from '@/features/auth/EnterpriseRegisterPage';
|
||||||
import { LoginPage } from '@/features/auth/LoginPage';
|
import { LoginPage } from '@/features/auth/LoginPage';
|
||||||
|
import { RegisterPage } from '@/features/auth/RegisterPage';
|
||||||
import { ResetPasswordPage } from '@/features/auth/ResetPasswordPage';
|
import { ResetPasswordPage } from '@/features/auth/ResetPasswordPage';
|
||||||
import { TenantEntryPage } from '@/features/auth/TenantEntryPage';
|
import { TenantEntryPage } from '@/features/auth/TenantEntryPage';
|
||||||
import { TenantFeedbackPage } from '@/features/auth/TenantFeedbackPage';
|
import { TenantFeedbackPage } from '@/features/auth/TenantFeedbackPage';
|
||||||
@@ -62,6 +63,7 @@ function RouteErrorBoundary() {
|
|||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{ path: 'tenant', element: <TenantEntryPage /> },
|
{ path: 'tenant', element: <TenantEntryPage /> },
|
||||||
{ path: 'login', element: <LoginPage /> },
|
{ path: 'login', element: <LoginPage /> },
|
||||||
|
{ path: 'register', element: <RegisterPage /> },
|
||||||
{ path: 'reset-password', element: <ResetPasswordPage /> },
|
{ path: 'reset-password', element: <ResetPasswordPage /> },
|
||||||
{ path: 'enterprise-register', element: <EnterpriseRegisterPage /> },
|
{ path: 'enterprise-register', element: <EnterpriseRegisterPage /> },
|
||||||
{ path: 'feedback', element: <TenantFeedbackPage /> },
|
{ path: 'feedback', element: <TenantFeedbackPage /> },
|
||||||
|
|||||||
@@ -112,6 +112,9 @@ export function LoginPage() {
|
|||||||
>
|
>
|
||||||
{submitting ? '登录中…' : '登录'}
|
{submitting ? '登录中…' : '登录'}
|
||||||
</button>
|
</button>
|
||||||
|
<p className="m-0 text-center text-sm text-text-secondary">
|
||||||
|
还没有账号?<Link to="/register" className="text-accent no-underline">注册账号</Link>
|
||||||
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { Link, Navigate, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Icon } from '@/app/Icon';
|
||||||
|
import { PasswordInput } from '@/features/auth/components/PasswordInput';
|
||||||
|
import { VerificationCodeButton } from '@/features/auth/components/VerificationCodeButton';
|
||||||
|
import { verifyRegistrationCode } from '@/lib/api/auth';
|
||||||
|
import { apiBase, ImApiError } from '@/lib/api/client';
|
||||||
|
import { getTinode, registerBasic } from '@/lib/tinode/connection';
|
||||||
|
import { loginErrorMessage, validateCredentialsMessage } from '@/lib/tinode/errors';
|
||||||
|
import { useSessionStore } from '@/stores/session';
|
||||||
|
|
||||||
|
const USERNAME_RE = /^[a-z0-9]{3,10}$/;
|
||||||
|
const PHONE_RE = /^\d{11}$/;
|
||||||
|
const CODE_RE = /^(?:\d{6}|[A-Za-z]{5})$/;
|
||||||
|
|
||||||
|
function normalizePhone(raw: string): string {
|
||||||
|
const digits = raw.replace(/\D/g, '');
|
||||||
|
return (digits.length > 11 && digits.startsWith('86') ? digits.slice(2) : digits).slice(0, 11);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RegisterPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const tenant = useSessionStore((s) => s.tenant);
|
||||||
|
const setMyUserId = useSessionStore((s) => s.setMyUserId);
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [phone, setPhone] = useState('');
|
||||||
|
const [code, setCode] = useState('');
|
||||||
|
const [challengeId, setChallengeId] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirm, setConfirm] = useState('');
|
||||||
|
const [agreed, setAgreed] = useState(false);
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const verifiedRef = useRef<{ key: string; response: string } | null>(null);
|
||||||
|
const submitRef = useRef(false);
|
||||||
|
|
||||||
|
if (!tenant) return <Navigate to="/tenant" replace />;
|
||||||
|
const minPasswordLength = tenant.passwordPolicy?.minLength ?? 8;
|
||||||
|
const legalOrigin = new URL(apiBase(), window.location.href).origin;
|
||||||
|
|
||||||
|
const submit = async (event: React.FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (submitRef.current) return;
|
||||||
|
setError('');
|
||||||
|
if (!username && !phone) return setError('请填写用户名或手机号');
|
||||||
|
if (username && !USERNAME_RE.test(username)) return setError('用户名须为 3~10 位小写字母或数字');
|
||||||
|
if (phone && !PHONE_RE.test(phone)) return setError('请输入 11 位手机号');
|
||||||
|
if (phone && (!challengeId || !CODE_RE.test(code))) return setError('请先获取验证码并填写 6 位短信验证码或 5 位邀请码');
|
||||||
|
if (password.length < minPasswordLength || (tenant.passwordPolicy?.maxLength && password.length > tenant.passwordPolicy.maxLength)) {
|
||||||
|
return setError(`密码长度须为 ${minPasswordLength}~${tenant.passwordPolicy?.maxLength ?? 64} 位`);
|
||||||
|
}
|
||||||
|
if (password !== confirm) return setError('两次输入的密码不一致');
|
||||||
|
if (!agreed) return setError('请先阅读并同意用户条款与隐私政策');
|
||||||
|
|
||||||
|
submitRef.current = true;
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
let credential: { meth: string; val: string; resp: string; params: { challenge_id: string } } | undefined;
|
||||||
|
if (phone) {
|
||||||
|
const key = `${phone}:${challengeId}:${code}`;
|
||||||
|
let response = verifiedRef.current?.key === key ? verifiedRef.current.response : '';
|
||||||
|
if (!response) {
|
||||||
|
const verified = await verifyRegistrationCode(challengeId, code);
|
||||||
|
response = verified.verification_mode === 'invite_code' ? code : verified.verification_token || '';
|
||||||
|
if (!response) throw new Error('验证结果无效,请重新获取验证码');
|
||||||
|
verifiedRef.current = { key, response };
|
||||||
|
}
|
||||||
|
credential = { meth: 'tel', val: phone, resp: response, params: { challenge_id: challengeId } };
|
||||||
|
}
|
||||||
|
const ctrl = await registerBasic({ username, phone, password, tenantCode: tenant.code, credential });
|
||||||
|
if (ctrl.code >= 300) {
|
||||||
|
setError(ctrl.text === 'validate credentials'
|
||||||
|
? validateCredentialsMessage(ctrl.params as { reason?: string; cred?: string[] } | undefined)
|
||||||
|
: '注册失败,请检查资料后重试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tinode = getTinode();
|
||||||
|
const token = tinode.getAuthToken()?.token;
|
||||||
|
const userId = tinode.getCurrentUserID();
|
||||||
|
if (!token || !userId) throw new Error('账号已创建,请返回登录页登录');
|
||||||
|
sessionStorage.setItem('im-session-token', token);
|
||||||
|
setMyUserId(userId);
|
||||||
|
navigate('/messages', { replace: true });
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof ImApiError ? err.message : loginErrorMessage(err));
|
||||||
|
} finally {
|
||||||
|
submitRef.current = false;
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-dvh flex-col bg-surface-page">
|
||||||
|
<header className="flex h-14 items-center border-b border-border-panel bg-white px-5">
|
||||||
|
<Link to="/login" className="flex items-center gap-1 text-sm text-text-secondary no-underline">
|
||||||
|
<Icon name="arrow_back_ios_new" className="text-[16px]" />返回登录
|
||||||
|
</Link>
|
||||||
|
<span className="ml-auto text-sm text-text-secondary">{tenant.name}</span>
|
||||||
|
</header>
|
||||||
|
<main className="flex flex-1 justify-center px-5 py-8">
|
||||||
|
<form onSubmit={submit} className="flex w-full max-w-[440px] flex-col gap-4">
|
||||||
|
<h1 className="m-0 text-[24px] font-semibold text-text-title">注册账号</h1>
|
||||||
|
{error && <div role="alert" className="rounded-md bg-error-strip-bg px-3 py-2 text-sm text-error-strip-text">{error}</div>}
|
||||||
|
<label className="flex flex-col gap-1 text-sm text-text-secondary">
|
||||||
|
用户名
|
||||||
|
<input value={username} onChange={(e) => setUsername(e.target.value.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 10))}
|
||||||
|
placeholder="3~10 位小写字母或数字,可不填" autoComplete="username"
|
||||||
|
className="h-[50px] rounded-md bg-surface-input px-3.5 text-[15px] text-text-body outline-none focus:border focus:border-focus-border" />
|
||||||
|
</label>
|
||||||
|
<label className="flex flex-col gap-1 text-sm text-text-secondary">
|
||||||
|
手机号
|
||||||
|
<input value={phone} onChange={(e) => {
|
||||||
|
const nextPhone = normalizePhone(e.target.value);
|
||||||
|
if (nextPhone !== phone) {
|
||||||
|
setPhone(nextPhone);
|
||||||
|
setChallengeId('');
|
||||||
|
setCode('');
|
||||||
|
verifiedRef.current = null;
|
||||||
|
}
|
||||||
|
}} placeholder="11 位手机号,可不填"
|
||||||
|
type="tel" inputMode="numeric" autoComplete="tel"
|
||||||
|
className="h-[50px] rounded-md bg-surface-input px-3.5 text-[15px] text-text-body outline-none focus:border focus:border-focus-border" />
|
||||||
|
</label>
|
||||||
|
{phone && <div className="flex min-w-0 items-end gap-2">
|
||||||
|
<label className="flex min-w-0 flex-1 flex-col gap-1 text-sm text-text-secondary">
|
||||||
|
验证码或邀请码
|
||||||
|
<input value={code} onChange={(e) => { setCode(e.target.value.slice(0, 6)); verifiedRef.current = null; }}
|
||||||
|
placeholder="6 位短信码 / 5 位邀请码" autoComplete="one-time-code"
|
||||||
|
className="h-[50px] w-full rounded-md bg-surface-input px-3.5 text-[15px] text-text-body outline-none focus:border focus:border-focus-border" />
|
||||||
|
</label>
|
||||||
|
<div className="flex h-[50px] items-center">
|
||||||
|
<VerificationCodeButton phone={phone} scene="register" tenantCode={tenant.code}
|
||||||
|
onChallenge={(id) => { setChallengeId(id); setCode(''); verifiedRef.current = null; }} onError={setError} />
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
{!phone && username && <p className="m-0 text-xs text-text-secondary">未绑定手机号时无法自助找回密码,请联系企业管理员重置。</p>}
|
||||||
|
<label className="text-sm text-text-secondary">密码</label>
|
||||||
|
<PasswordInput value={password} onChange={setPassword} placeholder={`至少 ${minPasswordLength} 位密码`}
|
||||||
|
autoComplete="new-password" maxLength={tenant.passwordPolicy?.maxLength} />
|
||||||
|
<label className="text-sm text-text-secondary">确认密码</label>
|
||||||
|
<PasswordInput value={confirm} onChange={setConfirm} placeholder="再次输入密码"
|
||||||
|
autoComplete="new-password" maxLength={tenant.passwordPolicy?.maxLength} />
|
||||||
|
<label className="flex items-start gap-2 text-sm text-text-secondary">
|
||||||
|
<input type="checkbox" checked={agreed} onChange={(e) => setAgreed(e.target.checked)} className="mt-1" />
|
||||||
|
<span>我已阅读并同意 <a href={`${legalOrigin}/legal/user-terms`} target="_blank" rel="noreferrer" className="text-accent">用户条款</a> 和 <a href={`${legalOrigin}/legal/privacy-policy`} target="_blank" rel="noreferrer" className="text-accent">隐私政策</a></span>
|
||||||
|
</label>
|
||||||
|
<button type="submit" disabled={submitting || !agreed || (!username && !phone)}
|
||||||
|
className="h-[50px] rounded-md bg-btn text-base font-medium text-white disabled:bg-btn-disabled">
|
||||||
|
{submitting ? '注册中…' : '注册并登录'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,16 +7,18 @@ const PHONE_RE = /^\d{11}$/;
|
|||||||
|
|
||||||
interface VerificationCodeButtonProps {
|
interface VerificationCodeButtonProps {
|
||||||
phone: string;
|
phone: string;
|
||||||
scene: 'reset_password';
|
scene: 'reset_password' | 'register';
|
||||||
|
tenantCode?: string;
|
||||||
onChallenge: (challengeId: string) => void;
|
onChallenge: (challengeId: string) => void;
|
||||||
onError: (message: string) => void;
|
onError: (message: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** "获取验证码" button with resend countdown. Ported from the old app's VerificationButton. */
|
/** "获取验证码" button with resend countdown. Ported from the old app's VerificationButton. */
|
||||||
export function VerificationCodeButton({ phone, scene, onChallenge, onError }: VerificationCodeButtonProps) {
|
export function VerificationCodeButton({ phone, scene, tenantCode, onChallenge, onError }: VerificationCodeButtonProps) {
|
||||||
const [seconds, setSeconds] = useState(0);
|
const [seconds, setSeconds] = useState(0);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const timerRef = useRef<number | undefined>(undefined);
|
const timerRef = useRef<number | undefined>(undefined);
|
||||||
|
const phoneRef = useRef(phone);
|
||||||
|
|
||||||
// Without this, navigating away mid-countdown (e.g. switching from register back to login)
|
// Without this, navigating away mid-countdown (e.g. switching from register back to login)
|
||||||
// leaves the interval running against an unmounted component indefinitely until it naturally
|
// leaves the interval running against an unmounted component indefinitely until it naturally
|
||||||
@@ -27,6 +29,7 @@ export function VerificationCodeButton({ phone, scene, onChallenge, onError }: V
|
|||||||
// otherwise correcting a mistyped number mid-cooldown leaves the button stuck disabled (still
|
// otherwise correcting a mistyped number mid-cooldown leaves the button stuck disabled (still
|
||||||
// counting down for the old number) until the old timer happens to run out on its own.
|
// counting down for the old number) until the old timer happens to run out on its own.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
phoneRef.current = phone;
|
||||||
window.clearInterval(timerRef.current);
|
window.clearInterval(timerRef.current);
|
||||||
setSeconds(0);
|
setSeconds(0);
|
||||||
}, [phone]);
|
}, [phone]);
|
||||||
@@ -55,12 +58,12 @@ export function VerificationCodeButton({ phone, scene, onChallenge, onError }: V
|
|||||||
const requestedPhone = phone;
|
const requestedPhone = phone;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
const data = await requestVerificationCode(scene, requestedPhone);
|
const data = await requestVerificationCode(scene, requestedPhone, tenantCode);
|
||||||
if (requestedPhone !== phone) return;
|
if (requestedPhone !== phoneRef.current) return;
|
||||||
onChallenge(data.challenge_id);
|
onChallenge(data.challenge_id);
|
||||||
tick(data.resend_after_seconds || 60);
|
tick(data.resend_after_seconds || 60);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (requestedPhone !== phone) return;
|
if (requestedPhone !== phoneRef.current) return;
|
||||||
onError(err instanceof ImApiError ? err.message : '获取验证码失败');
|
onError(err instanceof ImApiError ? err.message : '获取验证码失败');
|
||||||
// Rate-limited but a still-valid challenge already exists for this phone/scene -- reuse it.
|
// Rate-limited but a still-valid challenge already exists for this phone/scene -- reuse it.
|
||||||
const details = err instanceof ImApiError ? (err.details as Record<string, unknown> | undefined) : undefined;
|
const details = err instanceof ImApiError ? (err.details as Record<string, unknown> | undefined) : undefined;
|
||||||
|
|||||||
+19
-2
@@ -16,8 +16,25 @@ export interface VerificationCodeResponse {
|
|||||||
resend_after_seconds: number;
|
resend_after_seconds: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requestVerificationCode(scene: 'reset_password', phone: string) {
|
export function requestVerificationCode(scene: 'reset_password' | 'register', phone: string, tenantCode?: string) {
|
||||||
return imPost<VerificationCodeResponse>('/auth/verification-codes', { scene, phone }, { auth: false });
|
return imPost<VerificationCodeResponse>(
|
||||||
|
'/auth/verification-codes',
|
||||||
|
scene === 'register' ? { scene, phone, registration_type: 0, tenant_code: tenantCode } : { scene, phone },
|
||||||
|
{ auth: false },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistrationVerifyResponse {
|
||||||
|
verification_token?: string;
|
||||||
|
verification_mode?: 'invite_code';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyRegistrationCode(challengeId: string, code: string) {
|
||||||
|
return imPost<RegistrationVerifyResponse>(
|
||||||
|
`/auth/verification-codes/${encodeURIComponent(challengeId)}/verify`,
|
||||||
|
{ code },
|
||||||
|
{ auth: false },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PasswordResetVerifyResponse {
|
export interface PasswordResetVerifyResponse {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
// silently stomp on each other. This module claims all the SDK's callback slots exactly once and
|
// silently stomp on each other. This module claims all the SDK's callback slots exactly once and
|
||||||
// fans them out to any number of subscribers via its own Set, which is what hooks.ts's
|
// fans them out to any number of subscribers via its own Set, which is what hooks.ts's
|
||||||
// useSyncExternalStore-based hooks actually subscribe to.
|
// useSyncExternalStore-based hooks actually subscribe to.
|
||||||
import { Tinode, type LoginCtrl, type TinodeCtrl } from 'tinode-sdk';
|
import { Tinode, type CredentialParam, type LoginCtrl, type TinodeCtrl } from 'tinode-sdk';
|
||||||
|
|
||||||
import { API_KEY, APP_NAME, DEFAULT_HOST, LOGGING_ENABLED } from '@/lib/config';
|
import { API_KEY, APP_NAME, DEFAULT_HOST, LOGGING_ENABLED } from '@/lib/config';
|
||||||
import { isTenantMismatch } from '@/lib/tinode/errors';
|
import { isTenantMismatch } from '@/lib/tinode/errors';
|
||||||
@@ -166,6 +166,7 @@ export function clearKickedInfo() {
|
|||||||
let sessionTenantCode: string | null = null;
|
let sessionTenantCode: string | null = null;
|
||||||
/** One-shot override used by loginBasic's FORBIDDEN self-heal retry (see there). */
|
/** One-shot override used by loginBasic's FORBIDDEN self-heal retry (see there). */
|
||||||
let forcedLoginType: 0 | 1 | null = null;
|
let forcedLoginType: 0 | 1 | null = null;
|
||||||
|
let pendingRegistration: { username: string; phone: string; tenantCode: string } | null = null;
|
||||||
|
|
||||||
function installOutgoingPacketPatches(t: Tinode) {
|
function installOutgoingPacketPatches(t: Tinode) {
|
||||||
const conn = t._connection;
|
const conn = t._connection;
|
||||||
@@ -188,6 +189,19 @@ function installOutgoingPacketPatches(t: Tinode) {
|
|||||||
} catch {
|
} catch {
|
||||||
// Not valid JSON -- send as is.
|
// Not valid JSON -- send as is.
|
||||||
}
|
}
|
||||||
|
} else if (typeof msg === 'string' && pendingRegistration && msg.indexOf('"acc":') >= 0) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(msg);
|
||||||
|
if (parsed.acc?.user === 'new') {
|
||||||
|
parsed.acc.username = pendingRegistration.username;
|
||||||
|
parsed.acc.phone = pendingRegistration.phone;
|
||||||
|
parsed.acc.registration_type = 0;
|
||||||
|
parsed.acc.tenant_code = pendingRegistration.tenantCode;
|
||||||
|
msg = JSON.stringify(parsed);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not valid JSON -- send as is.
|
||||||
|
}
|
||||||
} else if (typeof msg === 'string' && msg.indexOf('"login":') >= 0) {
|
} else if (typeof msg === 'string' && msg.indexOf('"login":') >= 0) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(msg);
|
const parsed = JSON.parse(msg);
|
||||||
@@ -403,3 +417,32 @@ export function loginBasic(login: string, password: string): Promise<LoginCtrl>
|
|||||||
});
|
});
|
||||||
return loginPromise;
|
return loginPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let registrationPromise: Promise<LoginCtrl> | null = null;
|
||||||
|
|
||||||
|
export function registerBasic(input: {
|
||||||
|
username: string;
|
||||||
|
phone: string;
|
||||||
|
password: string;
|
||||||
|
tenantCode: string;
|
||||||
|
credential?: CredentialParam;
|
||||||
|
}): Promise<LoginCtrl> {
|
||||||
|
if (registrationPromise) return registrationPromise;
|
||||||
|
registrationPromise = (async () => {
|
||||||
|
await connect();
|
||||||
|
const t = getTinode();
|
||||||
|
const account = input.username || `+86${input.phone}`;
|
||||||
|
pendingRegistration = { username: input.username, phone: input.phone, tenantCode: input.tenantCode };
|
||||||
|
try {
|
||||||
|
return await t.createAccountBasic(account, input.password, {
|
||||||
|
public: { fn: input.username || account },
|
||||||
|
cred: input.credential ? [input.credential] : [],
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
pendingRegistration = null;
|
||||||
|
}
|
||||||
|
})().finally(() => {
|
||||||
|
registrationPromise = null;
|
||||||
|
});
|
||||||
|
return registrationPromise;
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default defineConfig({
|
|||||||
proxy: {
|
proxy: {
|
||||||
'/v1/im': { target: 'http://127.0.0.1:6060', changeOrigin: true, secure: false },
|
'/v1/im': { target: 'http://127.0.0.1:6060', changeOrigin: true, secure: false },
|
||||||
'/v0/file': { target: 'http://127.0.0.1:6060', changeOrigin: true, secure: false },
|
'/v0/file': { target: 'http://127.0.0.1:6060', changeOrigin: true, secure: false },
|
||||||
|
'/legal': { target: 'http://127.0.0.1:6060', changeOrigin: true, secure: false },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user