3cd322ccfe
Stop auto-creating duplicate accounts on OAuth, add bind-or-register gate, PC scan login, mobile open-in-WeChat guide, and fix localhost session cookies. Co-authored-by: Cursor <cursoragent@cursor.com>
185 lines
5.8 KiB
TypeScript
185 lines
5.8 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import {
|
||
completeWechatBind,
|
||
completeWechatRegister,
|
||
getMe,
|
||
getWechatPendingProfile,
|
||
} from '../api/client';
|
||
import type { CapabilityMap, PortalUser } from '../types';
|
||
import type { WechatPendingProfile } from '../api/client';
|
||
import { TKMindAvatar } from './TKMindAvatar';
|
||
|
||
type Step = 'choose' | 'bind';
|
||
|
||
export function WechatBindGate({
|
||
pendingToken,
|
||
returnTo,
|
||
onAuth,
|
||
}: {
|
||
pendingToken: string;
|
||
returnTo?: string | null;
|
||
onAuth: (
|
||
user?: PortalUser | null,
|
||
capabilities?: CapabilityMap,
|
||
grantedSkills?: string[],
|
||
) => void;
|
||
}) {
|
||
const [profile, setProfile] = useState<WechatPendingProfile | null>(null);
|
||
const [step, setStep] = useState<Step>('choose');
|
||
const [username, setUsername] = useState('');
|
||
const [password, setPassword] = useState('');
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
useEffect(() => {
|
||
void getWechatPendingProfile(pendingToken)
|
||
.then((data) => setProfile(data))
|
||
.catch((err) => {
|
||
setError(err instanceof Error ? err.message : '绑定会话无效');
|
||
})
|
||
.finally(() => setLoading(false));
|
||
}, [pendingToken]);
|
||
|
||
const finishAuth = async (user: PortalUser) => {
|
||
const me = await getMe().catch(() => null);
|
||
onAuth(user, me?.capabilities, me?.grantedSkills);
|
||
const target = returnTo || profile?.returnTo;
|
||
if (target && target !== '/') {
|
||
try {
|
||
const url = new URL(target, window.location.origin);
|
||
if (url.origin === window.location.origin) {
|
||
window.location.href = url.toString();
|
||
return;
|
||
}
|
||
} catch {
|
||
// ignore invalid return target
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleNewUser = async () => {
|
||
setSubmitting(true);
|
||
setError(null);
|
||
try {
|
||
const result = await completeWechatRegister(pendingToken);
|
||
await finishAuth(result.user);
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : '注册失败');
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const handleBindExisting = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setSubmitting(true);
|
||
setError(null);
|
||
try {
|
||
const result = await completeWechatBind(pendingToken, username, password);
|
||
await finishAuth(result.user);
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : '绑定失败');
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const displayName = profile?.nickname?.trim() || '微信用户';
|
||
|
||
return (
|
||
<div className="login-container">
|
||
<div className="login-card wechat-bind-card">
|
||
<div className="login-brand">
|
||
{profile?.avatarUrl ? (
|
||
<img src={profile.avatarUrl} alt="" className="wechat-bind-avatar" />
|
||
) : (
|
||
<TKMindAvatar size="md" className="login-brand-avatar" />
|
||
)}
|
||
<h1 className="login-title">你好,{displayName}</h1>
|
||
<p className="login-desc">
|
||
{step === 'choose'
|
||
? '请选择如何使用这个微信账号'
|
||
: '验证已有账号后,微信将与该账号绑定'}
|
||
</p>
|
||
</div>
|
||
|
||
{loading && <p className="login-desc">正在加载微信信息…</p>}
|
||
|
||
{!loading && step === 'choose' && (
|
||
<div className="wechat-bind-options">
|
||
<button
|
||
type="button"
|
||
className="wechat-bind-option wechat-bind-option-primary"
|
||
disabled={submitting}
|
||
onClick={() => void handleNewUser()}
|
||
>
|
||
<strong>我是新用户</strong>
|
||
<span>快速开始,稍后可设置密码</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="wechat-bind-option"
|
||
disabled={submitting}
|
||
onClick={() => {
|
||
setStep('bind');
|
||
setError(null);
|
||
}}
|
||
>
|
||
<strong>我已有账号</strong>
|
||
<span>绑定微信,保留原有内容</span>
|
||
</button>
|
||
<p className="wechat-bind-hint">
|
||
绑定后,微信登录将进入此账号,你的 MindSpace 内容不会丢失。
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{!loading && step === 'bind' && (
|
||
<form className="wechat-bind-form" onSubmit={handleBindExisting}>
|
||
<div className="login-fields">
|
||
<input
|
||
className={`login-input${error ? ' login-input-error' : ''}`}
|
||
type="text"
|
||
placeholder="用户名"
|
||
value={username}
|
||
autoComplete="username"
|
||
autoFocus
|
||
onChange={(e) => setUsername(e.target.value)}
|
||
/>
|
||
<input
|
||
className={`login-input${error ? ' login-input-error' : ''}`}
|
||
type="password"
|
||
placeholder="密码"
|
||
value={password}
|
||
autoComplete="current-password"
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
/>
|
||
</div>
|
||
{error && <p className="login-error">{error}</p>}
|
||
<button
|
||
type="submit"
|
||
className="login-btn"
|
||
disabled={submitting || !username.trim() || !password.trim()}
|
||
>
|
||
{submitting ? '绑定中…' : '验证并绑定'}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="login-link wechat-bind-back"
|
||
onClick={() => {
|
||
setStep('choose');
|
||
setError(null);
|
||
}}
|
||
>
|
||
返回选择
|
||
</button>
|
||
</form>
|
||
)}
|
||
|
||
{error && step === 'choose' && <p className="login-error">{error}</p>}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|