Files
memind/src/components/AuthView.tsx
T
John 6ee6fd64dd Add MindSpace page live edit, chat skills, and H5 deploy tooling.
Introduce page edit sessions with draft preview and patch API, chat skill picker, user memory profile, h5ApiBase resolution, voice WAV transport, and scripts for 105/g2 deployment and Plaza local dev.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 22:09:38 -07:00

486 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import {
getMe,
getWechatAuthConfig,
login,
loginLegacy,
register,
resetPassword,
} from '../api/client';
import type { CapabilityMap, PortalUser } from '../types';
import {
buildWechatAuthorizeUrl,
isMobileExternalBrowser,
isWechatContext,
isWechatEntryUrl,
readWechatAuthError,
readWechatPendingToken,
} from '../utils/wechat';
import { TKMindAvatar } from './TKMindAvatar';
import { resolvePlazaHomeUrl } from '../utils/publicUrl';
import { WechatBindGate } from './WechatBindGate';
import { WechatDesktopLogin } from './WechatDesktopLogin';
import { WechatIcon } from './WechatIcon';
import { WechatOpenGuide } from './WechatOpenGuide';
type Mode = 'login' | 'register' | 'reset';
const MODE_META: Record<Mode, { title: string; desc: string }> = {
login: { title: 'TKMind', desc: '登录你的账号' },
register: { title: '创建账号', desc: '填写信息完成注册' },
reset: { title: '重置密码', desc: '验证注册邮箱后设置新密码' },
};
export function AuthView({
legacyMode,
onAuth,
}: {
legacyMode?: boolean;
onAuth: (
user?: PortalUser | null,
capabilities?: CapabilityMap,
grantedSkills?: string[],
) => void;
}) {
const [mode, setMode] = useState<Mode>('login');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [displayName, setDisplayName] = useState('');
const [email, setEmail] = useState('');
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
const [wechatEnabled, setWechatEnabled] = useState(false);
const [wechatContext, setWechatContext] = useState(() =>
isWechatContext({ search: window.location.search }),
);
const [mobileExternal, setMobileExternal] = useState(() =>
isMobileExternalBrowser({ search: window.location.search }),
);
const [showPasswordLogin, setShowPasswordLogin] = useState(false);
const [showScanLogin, setShowScanLogin] = useState(false);
const [pendingToken, setPendingToken] = useState<string | null>(() => readWechatPendingToken());
const searchParams = new URLSearchParams(window.location.search);
const fromPlaza = (searchParams.get('utm_source') ?? '').toLowerCase() === 'plaza';
const returnTo = searchParams.get('return_to');
useEffect(() => {
const wechatError = readWechatAuthError();
if (wechatError) {
setError(wechatError);
}
}, []);
useEffect(() => {
if (legacyMode) return;
void getWechatAuthConfig().then((config) => {
setWechatEnabled(config.enabled);
setWechatContext((prev) =>
isWechatContext({
search: window.location.search,
serverInWechat: config.inWechat,
}) || prev,
);
setMobileExternal(
isMobileExternalBrowser({
search: window.location.search,
serverInWechat: config.inWechat,
}),
);
const params = new URLSearchParams(window.location.search);
if (
config.enabled &&
isWechatEntryUrl(window.location.search) &&
isWechatContext({
search: window.location.search,
serverInWechat: config.inWechat,
}) &&
!params.get('wechat_pending') &&
!params.get('wechat_error')
) {
window.location.replace(
buildWechatAuthorizeUrl({
returnTo: returnTo ?? undefined,
utmSource: params.get('utm_source') ?? params.get('from') ?? 'wechat',
utmMedium: params.get('utm_medium') ?? undefined,
utmCampaign: params.get('utm_campaign') ?? undefined,
intent: 'login',
}),
);
}
});
}, [legacyMode, returnTo]);
const isLogin = mode === 'login' && !legacyMode;
const showWechatOneClick = isLogin && wechatContext && wechatEnabled;
const showMobileWechatGuide = isLogin && mobileExternal && wechatEnabled;
const showDesktopWechat = isLogin && wechatEnabled && !wechatContext && !mobileExternal;
const isDesktopScanMode = showDesktopWechat && showScanLogin;
const showAccountForm =
!isDesktopScanMode &&
(legacyMode ||
mode !== 'login' ||
mobileExternal ||
showPasswordLogin ||
!showWechatOneClick ||
showDesktopWechat);
const switchToPasswordLogin = () => {
setShowScanLogin(false);
setError(null);
setSuccess(null);
};
const switchToScanLogin = () => {
setShowScanLogin(true);
setError(null);
setSuccess(null);
};
const switchMode = (next: Mode) => {
setMode(next);
setError(null);
setSuccess(null);
setConfirmPassword('');
setShowScanLogin(false);
};
const handleWechatLogin = () => {
const params = new URLSearchParams(window.location.search);
window.location.href = buildWechatAuthorizeUrl({
returnTo: returnTo ?? undefined,
utmSource: params.get('utm_source') ?? params.get('from') ?? 'wechat',
utmMedium: params.get('utm_medium') ?? undefined,
utmCampaign: params.get('utm_campaign') ?? undefined,
intent: 'login',
});
};
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setSubmitting(true);
setError(null);
try {
if (legacyMode) {
await loginLegacy(password);
onAuth(null);
return;
}
const user = await login(username, password);
const me = await getMe().catch(() => null);
onAuth(user, me?.capabilities, me?.grantedSkills);
} catch (err) {
setError(err instanceof Error ? err.message : '登录失败');
} finally {
setSubmitting(false);
}
};
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
setSubmitting(true);
setError(null);
try {
await register(
username,
password,
displayName || undefined,
email,
{
utm_source: searchParams.get('utm_source') ?? undefined,
utm_medium: searchParams.get('utm_medium') ?? undefined,
utm_campaign: searchParams.get('utm_campaign') ?? undefined,
ref: searchParams.get('ref') ?? undefined,
},
);
const user = await login(username, password);
const me = await getMe().catch(() => null);
sessionStorage.setItem('tkmind_suggest_wechat_bind', '1');
onAuth(user, me?.capabilities, me?.grantedSkills);
} catch (err) {
setError(err instanceof Error ? err.message : '注册失败');
} finally {
setSubmitting(false);
}
};
const handleReset = async (e: React.FormEvent) => {
e.preventDefault();
if (password !== confirmPassword) {
setError('两次输入的密码不一致');
return;
}
setSubmitting(true);
setError(null);
setSuccess(null);
try {
await resetPassword(username, email, password);
setSuccess('密码已重置,请使用新密码登录');
setPassword('');
setConfirmPassword('');
setTimeout(() => switchMode('login'), 1500);
} catch (err) {
setError(err instanceof Error ? err.message : '重置失败');
} finally {
setSubmitting(false);
}
};
if (pendingToken) {
return (
<WechatBindGate
pendingToken={pendingToken}
returnTo={returnTo}
onAuth={onAuth}
/>
);
}
const meta = legacyMode
? { title: 'TKMind', desc: '内部访问入口' }
: fromPlaza
? {
...MODE_META[mode],
desc:
mode === 'register'
? '注册后将返回 Plaza 继续浏览'
: '登录后将返回 Plaza 继续浏览',
}
: MODE_META[mode];
const plazaBackUrl = (() => {
if (!returnTo) return resolvePlazaHomeUrl();
try {
return new URL(returnTo, window.location.origin).toString();
} catch {
return resolvePlazaHomeUrl();
}
})();
const onSubmit =
mode === 'register' && !legacyMode
? handleRegister
: mode === 'reset' && !legacyMode
? handleReset
: handleLogin;
const submitLabel =
submitting
? '处理中…'
: legacyMode
? '解锁'
: mode === 'register'
? '注册并登录'
: mode === 'reset'
? '确认重置'
: '登录';
const submitDisabled =
submitting ||
(legacyMode
? !password.trim()
: mode === 'login'
? !username.trim() || !password.trim()
: mode === 'register'
? !username.trim() || !password.trim() || !email.trim()
: !username.trim() ||
!email.trim() ||
!password.trim() ||
!confirmPassword.trim());
const showAltLogin =
isLogin && wechatEnabled && (showMobileWechatGuide || (showDesktopWechat && !showScanLogin));
return (
<div className="login-container">
<div className="login-card">
{!legacyMode && fromPlaza && (
<div className="login-plaza-back">
<a href={plazaBackUrl}> 广</a>
</div>
)}
{!legacyMode && (
<div className="login-topbar">
{mode === 'login' ? (
<>
<button type="button" className="login-link" onClick={() => switchMode('register')}>
</button>
<span className="login-topbar-sep" aria-hidden="true" />
<button type="button" className="login-link" onClick={() => switchMode('reset')}>
</button>
</>
) : (
<button type="button" className="login-link" onClick={() => switchMode('login')}>
</button>
)}
</div>
)}
<form className="login-form" onSubmit={onSubmit}>
<div className="login-brand">
<TKMindAvatar size="md" className="login-brand-avatar" />
<h1 className="login-title">{meta.title}</h1>
<p className="login-desc">{meta.desc}</p>
</div>
{isDesktopScanMode && (
<>
<WechatDesktopLogin
returnTo={returnTo}
onBindingGate={(token) => setPendingToken(token)}
onComplete={() => window.location.reload()}
onError={(message) => setError(message)}
/>
{error && <p className="login-error">{error}</p>}
{success && <p className="login-success">{success}</p>}
<button type="button" className="login-mode-switch" onClick={switchToPasswordLogin}>
</button>
</>
)}
{showWechatOneClick && (
<>
<button
type="button"
className="login-wechat-btn"
onClick={handleWechatLogin}
disabled={submitting}
>
<WechatIcon className="login-wechat-icon" />
</button>
{!showPasswordLogin ? (
<button
type="button"
className="login-link login-password-toggle"
onClick={() => setShowPasswordLogin(true)}
>
</button>
) : (
<div className="login-divider" aria-hidden="true">
<span></span>
</div>
)}
</>
)}
{showAccountForm && (
<>
<div className="login-fields">
{!legacyMode && (
<input
className={`login-input${error ? ' login-input-error' : ''}`}
type="text"
placeholder="用户名"
value={username}
autoComplete="username"
autoFocus={isLogin}
onChange={(e) => setUsername(e.target.value)}
/>
)}
{mode === 'register' && !legacyMode && (
<>
<input
className="login-input"
type="email"
placeholder="邮箱"
value={email}
autoComplete="email"
required
onChange={(e) => setEmail(e.target.value)}
/>
<input
className="login-input"
type="text"
placeholder="显示名称(可选)"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
/>
</>
)}
{mode === 'reset' && !legacyMode && (
<input
className="login-input"
type="email"
placeholder="注册邮箱"
value={email}
autoComplete="email"
required
onChange={(e) => setEmail(e.target.value)}
/>
)}
<input
className={`login-input${error ? ' login-input-error' : ''}`}
type="password"
placeholder={mode === 'reset' ? '新密码(至少 6 位)' : '密码'}
value={password}
autoComplete={
mode === 'register' || mode === 'reset' ? 'new-password' : 'current-password'
}
autoFocus={legacyMode}
onChange={(e) => setPassword(e.target.value)}
/>
{mode === 'reset' && !legacyMode && (
<input
className={`login-input${error ? ' login-input-error' : ''}`}
type="password"
placeholder="确认新密码"
value={confirmPassword}
autoComplete="new-password"
onChange={(e) => setConfirmPassword(e.target.value)}
/>
)}
</div>
{error && <p className="login-error">{error}</p>}
{success && <p className="login-success">{success}</p>}
<button type="submit" className="login-btn" disabled={submitDisabled}>
{submitLabel}
</button>
</>
)}
{showAltLogin && (
<div className="login-alt-methods">
<div className="login-divider" aria-hidden="true">
<span></span>
</div>
{showDesktopWechat && (
<button
type="button"
className="login-wechat-secondary"
onClick={switchToScanLogin}
disabled={submitting}
>
<WechatIcon className="login-wechat-icon" />
</button>
)}
{showMobileWechatGuide && (
<WechatOpenGuide
disabled={submitting}
onCopied={() => setSuccess('链接已复制,请在微信中打开')}
/>
)}
</div>
)}
</form>
</div>
</div>
);
}