import React, { useEffect, useLayoutEffect, useRef, useState } from "react"; import { API_BASE, authClientFetchHeaders, buildAuthCallbackUrl, clearAuthClientContext, fetchClientVisitMeta, formatAuthBanMessage, getOAuthReturnTo, normalizeSelfServiceAccountInput, persistAuthClientFromFlow, randomSelfServiceAccount } from "../config"; import UserPortalAuthSection from "./userPortal/UserPortalAuthSection"; import UserPortalProfileSection from "./userPortal/UserPortalProfileSection"; import OAuthConsentScreen from "./OAuthConsentScreen"; export default function UserPortal({ onReady, authFlow, onPreviewImage, oauthStandalone = false, onGuestAuthLayoutChange }) { const [account, setAccount] = useState(""); const [password, setPassword] = useState(""); const [user, setUser] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [mode, setMode] = useState("login"); const [registerForm, setRegisterForm] = useState({ account: "", password: "", username: "", email: "", code: "", inviteCode: "" }); const [registrationRequireInvite, setRegistrationRequireInvite] = useState(false); const [inviteRegisterReward, setInviteRegisterReward] = useState(10); const [registrationAccountMin, setRegistrationAccountMin] = useState(3); const [registrationAccountMax, setRegistrationAccountMax] = useState(32); const [registerSent, setRegisterSent] = useState(false); const [registerExpiresAt, setRegisterExpiresAt] = useState(""); const [registerLoading, setRegisterLoading] = useState(false); const [registerError, setRegisterError] = useState(""); const [registerMessage, setRegisterMessage] = useState(""); const [resetForm, setResetForm] = useState({ account: "", email: "", code: "", newPassword: "" }); const [resetSent, setResetSent] = useState(false); const [resetExpiresAt, setResetExpiresAt] = useState(""); const [resetLoading, setResetLoading] = useState(false); const [resetError, setResetError] = useState(""); const [resetMessage, setResetMessage] = useState(""); const [secondaryForm, setSecondaryForm] = useState({ email: "", code: "" }); const [secondarySent, setSecondarySent] = useState(false); const [secondaryExpiresAt, setSecondaryExpiresAt] = useState(""); const [secondaryLoading, setSecondaryLoading] = useState(false); const [secondaryError, setSecondaryError] = useState(""); const [secondaryMessage, setSecondaryMessage] = useState(""); const [profileForm, setProfileForm] = useState({ username: "", phone: "", avatarUrl: "", websiteUrl: "", bio: "", password: "" }); const [profileLoading, setProfileLoading] = useState(false); const [profileError, setProfileError] = useState(""); const [profileMessage, setProfileMessage] = useState(""); const [profileEditorOpen, setProfileEditorOpen] = useState(false); const [profileEditorFocus, setProfileEditorFocus] = useState(""); const [secondaryEditorOpen, setSecondaryEditorOpen] = useState(false); const [checkInReward, setCheckInReward] = useState(1); const [checkInToday, setCheckInToday] = useState(false); const [checkInLoading, setCheckInLoading] = useState(false); const [checkInError, setCheckInError] = useState(""); const [checkInMessage, setCheckInMessage] = useState(""); const [githubOAuthEnabled, setGithubOAuthEnabled] = useState(false); const [giteaOAuthEnabled, setGiteaOAuthEnabled] = useState(false); const [googleOAuthEnabled, setGoogleOAuthEnabled] = useState(false); const [microsoftOAuthEnabled, setMicrosoftOAuthEnabled] = useState(false); const [linuxdoOAuthEnabled, setLinuxdoOAuthEnabled] = useState(false); const [oauthLogos, setOauthLogos] = useState({ github: "", gitea: "", google: "", microsoft: "", linuxdo: "" }); const [turnstileEnabled, setTurnstileEnabled] = useState(false); const [turnstileSiteKey, setTurnstileSiteKey] = useState(""); const [loginTurnstileToken, setLoginTurnstileToken] = useState(""); const [registerTurnstileToken, setRegisterTurnstileToken] = useState(""); const loginTurnstileRef = useRef(null); const registerTurnstileRef = useRef(null); const isAuthFlow = Boolean(authFlow?.redirectUri); const redirectToAuthCallback = (tokenValue, userData, expiresAt = "") => { if (!isAuthFlow || !authFlow?.redirectUri || !tokenValue) return; const callbackUrl = buildAuthCallbackUrl(authFlow.redirectUri, { token: tokenValue, account: userData?.account || "", username: userData?.username || "", expiresAt, state: authFlow.state || "" }); window.location.replace(callbackUrl); }; const clearTokenAndUser = () => { localStorage.removeItem("sproutgate_token"); localStorage.removeItem("sproutgate_token_expires_at"); setUser(null); setProfileForm({ username: "", phone: "", avatarUrl: "", websiteUrl: "", bio: "", password: "" }); }; const bearerHeaders = (tokenValue) => ({ ...authClientFetchHeaders(), Authorization: `Bearer ${tokenValue}` }); const syncCurrentUser = async (tokenValue) => { const baseHeaders = bearerHeaders(tokenValue); const res = await fetch(`${API_BASE}/api/auth/me`, { headers: baseHeaders }); const data = await res.json(); if (!res.ok) { if (res.status === 403) { clearTokenAndUser(); const msg = formatAuthBanMessage(data); setError(msg); throw new Error(msg); } throw new Error(data.error || "加载用户失败"); } if (data.user) setUser(data.user); const checkIn = data.checkIn || {}; setCheckInReward(Number(checkIn.rewardCoins) || 1); setCheckInToday(Boolean(checkIn.checkedInToday)); fetchClientVisitMeta() .then((meta) => { if (!meta.ip && !meta.displayLocation) return null; const h = { ...baseHeaders }; if (meta.ip) h["X-Visit-Ip"] = meta.ip; if (meta.displayLocation) h["X-Visit-Location"] = meta.displayLocation; return fetch(`${API_BASE}/api/auth/me`, { headers: h }); }) .then(async (r) => { if (!r) return null; const d = await r.json().catch(() => ({})); if (r.status === 403) { clearTokenAndUser(); setError(formatAuthBanMessage(d)); return null; } return r.ok ? d : null; }) .then((d) => { if (!d) return; if (d.user) setUser(d.user); const c = d.checkIn; if (c) { setCheckInReward(Number(c.rewardCoins) || 1); setCheckInToday(Boolean(c.checkedInToday)); } }) .catch(() => {}); return data; }; useEffect(() => { let cancelled = false; const done = () => { if (!cancelled && onReady) onReady(); }; const hash = (window.location.hash || "").replace(/^#/, ""); if (hash) { const p = new URLSearchParams(hash); const ot = p.get("oauth_token"); if (ot) { const ex = p.get("oauth_expires_at") || ""; localStorage.setItem("sproutgate_token", ot); localStorage.setItem("sproutgate_token_expires_at", ex); window.history.replaceState(null, "", window.location.pathname + window.location.search); syncCurrentUser(ot).catch(() => {}).finally(() => done()); return () => { cancelled = true; }; } if (p.get("oauth_error")) { let desc = p.get("oauth_error_description") || "OAuth 登录失败"; try { desc = decodeURIComponent(desc.replace(/\+/g, " ")); } catch { /* use raw */ } setError(desc); window.history.replaceState(null, "", window.location.pathname + window.location.search); done(); return () => { cancelled = true; }; } if (p.get("oauth_bound")) { window.history.replaceState(null, "", window.location.pathname + window.location.search); const t = localStorage.getItem("sproutgate_token"); if (t) { syncCurrentUser(t).catch(() => {}).finally(() => done()); } else { done(); } return () => { cancelled = true; }; } } const token = localStorage.getItem("sproutgate_token"); if (token) { syncCurrentUser(token).catch(() => {}).finally(() => done()); } else { done(); } return () => { cancelled = true; }; }, [onReady]); useEffect(() => { persistAuthClientFromFlow(authFlow); }, [authFlow]); useLayoutEffect(() => { if (!onGuestAuthLayoutChange) return; const invalidOAuthStandalone = Boolean( oauthStandalone && !(authFlow?.redirectUri || "").trim() ); onGuestAuthLayoutChange(Boolean(!user && !invalidOAuthStandalone)); }, [user, oauthStandalone, authFlow, onGuestAuthLayoutChange]); useEffect(() => { let cancelled = false; (async () => { try { const res = await fetch(`${API_BASE}/api/public/registration-policy`); const data = await res.json().catch(() => ({})); if (!cancelled && res.ok) { setRegistrationRequireInvite(Boolean(data.requireInviteCode)); const ir = Number(data.inviteRegisterRewardCoins); if (Number.isFinite(ir) && ir >= 0) setInviteRegisterReward(ir); const am = Number(data.selfServiceAccountMax); const ami = Number(data.selfServiceAccountMin); if (Number.isFinite(am) && am >= 3 && am <= 32) setRegistrationAccountMax(am); if (Number.isFinite(ami) && ami >= 3 && ami <= 32) setRegistrationAccountMin(ami); setGithubOAuthEnabled(Boolean(data.githubOAuthEnabled)); setGiteaOAuthEnabled(Boolean(data.giteaOAuthEnabled)); setGoogleOAuthEnabled(Boolean(data.googleOAuthEnabled)); setMicrosoftOAuthEnabled(Boolean(data.microsoftOAuthEnabled)); setLinuxdoOAuthEnabled(Boolean(data.linuxdoOAuthEnabled)); setOauthLogos({ github: String(data.githubOAuthLogoUrl || "").trim(), gitea: String(data.giteaOAuthLogoUrl || "").trim(), google: String(data.googleOAuthLogoUrl || "").trim(), microsoft: String(data.microsoftOAuthLogoUrl || "").trim(), linuxdo: String(data.linuxdoOAuthLogoUrl || "").trim() }); setTurnstileEnabled(Boolean(data.turnstileEnabled)); setTurnstileSiteKey(String(data.turnstileSiteKey || "").trim()); } } catch { /* 忽略,默认不要求邀请码 */ } })(); return () => { cancelled = true; }; }, []); // 仅在外部 user 更新且资料弹窗关闭时同步表单,避免 /api/auth/me 二次请求在编辑期间把表单项覆盖回服务器值(QQ 用户会把已填写的自定义头像链接清空) useEffect(() => { if (!user || profileEditorOpen) return; setProfileForm({ username: user.username || "", phone: user.phone || "", avatarUrl: user.customAvatarUrl ?? "", websiteUrl: user.websiteUrl || "", bio: user.bio || "", password: "" }); }, [user, profileEditorOpen]); const handleLogin = async (event) => { event.preventDefault(); setLoading(true); setError(""); try { const loginPayload = { account, password }; if (turnstileEnabled) loginPayload.turnstileToken = loginTurnstileToken; const cid = (authFlow?.clientId || "").trim(); const cname = (authFlow?.clientName || "").trim(); if (cid) { loginPayload.clientId = cid; if (cname) loginPayload.clientName = cname; } else { const h = authClientFetchHeaders(); if (h["X-Auth-Client"]) { loginPayload.clientId = h["X-Auth-Client"]; if (h["X-Auth-Client-Name"]) loginPayload.clientName = h["X-Auth-Client-Name"]; } } const res = await fetch(`${API_BASE}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(loginPayload) }); const data = await res.json(); if (!res.ok) { throw new Error( res.status === 403 ? formatAuthBanMessage(data) : (data.error || "登录失败") ); } localStorage.setItem("sproutgate_token", data.token); localStorage.setItem("sproutgate_token_expires_at", data.expiresAt || ""); setUser(data.user); syncCurrentUser(data.token).catch(() => {}); setAccount(""); setPassword(""); if (isAuthFlow && !oauthStandalone) { redirectToAuthCallback(data.token, data.user, data.expiresAt || ""); return; } } catch (err) { setError(err.message); setLoginTurnstileToken(""); loginTurnstileRef.current?.reset(); } finally { setLoading(false); } }; const handleLogout = () => { clearAuthClientContext(); clearTokenAndUser(); setProfileEditorOpen(false); setProfileEditorFocus(""); setSecondaryEditorOpen(false); setCheckInReward(1); setCheckInToday(false); setCheckInError(""); setCheckInMessage(""); }; const handleContinueAuth = () => { const tokenValue = localStorage.getItem("sproutgate_token") || ""; if (!tokenValue) { setError("当前没有可用的登录会话,请先重新登录"); return; } redirectToAuthCallback( tokenValue, user, localStorage.getItem("sproutgate_token_expires_at") || "" ); }; const handleSwitchAuthAccount = () => { handleLogout(); setMode("login"); setAccount(""); setPassword(""); setError(""); }; const handleRegisterChange = (field, value) => { if (field === "account") { value = normalizeSelfServiceAccountInput(value, registrationAccountMax); } setRegisterForm((prev) => ({ ...prev, [field]: value })); }; const resetRegisterFlow = () => { setRegisterSent(false); setRegisterExpiresAt(""); setRegisterForm({ account: randomSelfServiceAccount(10), password: "", username: "", email: "", code: "", inviteCode: "" }); }; const handleSendCode = async (event) => { event.preventDefault(); setRegisterLoading(true); setRegisterError(""); setRegisterMessage(""); if (!registerForm.account || !registerForm.password || !registerForm.email) { setRegisterError("请填写账户、密码和邮箱"); setRegisterLoading(false); return; } if (registerForm.account.length < registrationAccountMin) { setRegisterError(`账户至少 ${registrationAccountMin} 位,仅小写字母与数字`); setRegisterLoading(false); return; } if (registrationRequireInvite && !String(registerForm.inviteCode || "").trim()) { setRegisterError("请输入邀请码"); setRegisterLoading(false); return; } try { const payload = { account: registerForm.account, password: registerForm.password, username: registerForm.username, email: registerForm.email }; if (turnstileEnabled) payload.turnstileToken = registerTurnstileToken; const inv = String(registerForm.inviteCode || "").trim(); if (inv) payload.inviteCode = inv; const res = await fetch(`${API_BASE}/api/auth/register`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "发送验证码失败"); setRegisterSent(true); setRegisterExpiresAt(data.expiresAt || ""); setRegisterMessage("验证码已发送,请检查邮箱"); } catch (err) { setRegisterError(err.message); setRegisterTurnstileToken(""); registerTurnstileRef.current?.reset(); } finally { setRegisterLoading(false); } }; const handleVerifyRegister = async (event) => { event.preventDefault(); setRegisterLoading(true); setRegisterError(""); setRegisterMessage(""); if (!registerForm.account || !registerForm.code) { setRegisterError("请输入账户与验证码"); setRegisterLoading(false); return; } try { const res = await fetch(`${API_BASE}/api/auth/verify-email`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ account: registerForm.account, code: registerForm.code }) }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "验证失败"); const coins = Number(data.user?.sproutCoins); setRegisterMessage( Number.isFinite(coins) && coins > 0 ? `注册成功,已获得 ${coins} 萌芽币,请使用账号登录` : "注册成功,请使用账号登录" ); setRegisterSent(false); setRegisterForm({ account: "", password: "", username: "", email: "", code: "", inviteCode: "" }); setMode("login"); } catch (err) { setRegisterError(err.message); } finally { setRegisterLoading(false); } }; const handleResetChange = (field, value) => { setResetForm((prev) => ({ ...prev, [field]: value })); }; const handleSendReset = async (event) => { event.preventDefault(); setResetLoading(true); setResetError(""); setResetMessage(""); if (!resetForm.account || !resetForm.email) { setResetError("请填写账户与邮箱"); setResetLoading(false); return; } try { const res = await fetch(`${API_BASE}/api/auth/forgot-password`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ account: resetForm.account, email: resetForm.email }) }); const data = await res.json(); if (!res.ok) { throw new Error( res.status === 403 ? formatAuthBanMessage(data) : (data.error || "发送重置邮件失败") ); } setResetSent(true); setResetExpiresAt(data.expiresAt || ""); setResetMessage("重置验证码已发送,请检查邮箱"); } catch (err) { setResetError(err.message); } finally { setResetLoading(false); } }; const handleResetPassword = async (event) => { event.preventDefault(); setResetLoading(true); setResetError(""); setResetMessage(""); if (!resetForm.account || !resetForm.code || !resetForm.newPassword) { setResetError("请填写账户、验证码与新密码"); setResetLoading(false); return; } try { const res = await fetch(`${API_BASE}/api/auth/reset-password`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ account: resetForm.account, code: resetForm.code, newPassword: resetForm.newPassword }) }); const data = await res.json(); if (!res.ok) { throw new Error( res.status === 403 ? formatAuthBanMessage(data) : (data.error || "重置失败") ); } setResetMessage("密码已重置,请使用新密码登录"); setResetSent(false); setResetForm({ account: "", email: "", code: "", newPassword: "" }); setMode("login"); } catch (err) { setResetError(err.message); } finally { setResetLoading(false); } }; const handleSecondaryChange = (field, value) => { setSecondaryForm((prev) => ({ ...prev, [field]: value })); }; const handleSendSecondary = async (event) => { event.preventDefault(); setSecondaryLoading(true); setSecondaryError(""); setSecondaryMessage(""); const token = localStorage.getItem("sproutgate_token"); if (!token) { setSecondaryError("请先登录"); setSecondaryLoading(false); return; } if (!secondaryForm.email) { setSecondaryError("请输入辅助邮箱"); setSecondaryLoading(false); return; } try { const res = await fetch(`${API_BASE}/api/auth/secondary-email/request`, { method: "POST", headers: { "Content-Type": "application/json", ...bearerHeaders(token) }, body: JSON.stringify({ email: secondaryForm.email }) }); const data = await res.json(); if (!res.ok) { if (res.status === 403) { clearTokenAndUser(); setSecondaryError(formatAuthBanMessage(data)); } else { throw new Error(data.error || "发送验证码失败"); } return; } setSecondarySent(true); setSecondaryExpiresAt(data.expiresAt || ""); setSecondaryMessage("验证码已发送,请检查邮箱"); } catch (err) { setSecondaryError(err.message); } finally { setSecondaryLoading(false); } }; const handleVerifySecondary = async (event) => { event.preventDefault(); setSecondaryLoading(true); setSecondaryError(""); setSecondaryMessage(""); const token = localStorage.getItem("sproutgate_token"); if (!token) { setSecondaryError("请先登录"); setSecondaryLoading(false); return; } if (!secondaryForm.email || !secondaryForm.code) { setSecondaryError("请输入邮箱与验证码"); setSecondaryLoading(false); return; } try { const res = await fetch(`${API_BASE}/api/auth/secondary-email/verify`, { method: "POST", headers: { "Content-Type": "application/json", ...bearerHeaders(token) }, body: JSON.stringify({ email: secondaryForm.email, code: secondaryForm.code }) }); const data = await res.json(); if (!res.ok) { if (res.status === 403) { clearTokenAndUser(); setSecondaryError(formatAuthBanMessage(data)); } else { throw new Error(data.error || "验证失败"); } return; } if (data.user) setUser(data.user); setSecondaryMessage("辅助邮箱验证成功"); setSecondarySent(false); setSecondaryForm({ email: "", code: "" }); setSecondaryEditorOpen(false); } catch (err) { setSecondaryError(err.message); } finally { setSecondaryLoading(false); } }; const handleProfileChange = (field, value) => { setProfileForm((prev) => ({ ...prev, [field]: value })); }; const handleProfileSave = async (event) => { event.preventDefault(); setProfileLoading(true); setProfileError(""); setProfileMessage(""); const token = localStorage.getItem("sproutgate_token"); if (!token) { setProfileError("请先登录"); setProfileLoading(false); return; } const payload = { username: profileForm.username, phone: profileForm.phone, avatarUrl: profileForm.avatarUrl, websiteUrl: profileForm.websiteUrl, bio: profileForm.bio }; if (profileForm.password) payload.password = profileForm.password; try { const res = await fetch(`${API_BASE}/api/auth/profile`, { method: "PUT", headers: { "Content-Type": "application/json", ...bearerHeaders(token) }, body: JSON.stringify(payload) }); const data = await res.json(); if (!res.ok) { if (res.status === 403) { clearTokenAndUser(); setProfileError(formatAuthBanMessage(data)); } else { throw new Error(data.error || "保存失败"); } return; } setUser(data.user); setProfileMessage("保存成功"); setProfileForm((prev) => ({ ...prev, password: "" })); setProfileEditorOpen(false); setProfileEditorFocus(""); } catch (err) { setProfileError(err.message); } finally { setProfileLoading(false); } }; const openProfileEditor = (field = "") => { setProfileError(""); setProfileMessage(""); if (user) { setProfileForm({ username: user.username || "", phone: user.phone || "", avatarUrl: user.customAvatarUrl ?? "", websiteUrl: user.websiteUrl || "", bio: user.bio || "", password: "" }); } setProfileEditorFocus(field); setProfileEditorOpen(true); }; const openSecondaryEditor = () => { setSecondaryError(""); setSecondaryMessage(""); setSecondarySent(false); setSecondaryForm({ email: "", code: "" }); setSecondaryEditorOpen(true); }; const closeProfileEditor = () => { setProfileEditorOpen(false); setProfileEditorFocus(""); }; const closeSecondaryEditor = () => { setSecondaryEditorOpen(false); setSecondarySent(false); setSecondaryForm({ email: "", code: "" }); setSecondaryError(""); setSecondaryMessage(""); }; const profileFocusLabels = { phone: "手机号", avatarUrl: "头像", websiteUrl: "个人主页", bio: "简介", username: "用户名" }; const profileModalTitle = profileEditorFocus && profileFocusLabels[profileEditorFocus] ? `修改${profileFocusLabels[profileEditorFocus]}` : "修改资料"; const handleCheckIn = async () => { const token = localStorage.getItem("sproutgate_token"); if (!token) { setCheckInError("请先登录"); return; } setCheckInLoading(true); setCheckInError(""); setCheckInMessage(""); try { const res = await fetch(`${API_BASE}/api/auth/check-in`, { method: "POST", headers: bearerHeaders(token) }); const data = await res.json(); if (!res.ok) { if (res.status === 403) { clearTokenAndUser(); setCheckInError(formatAuthBanMessage(data)); } else { throw new Error(data.error || "签到失败"); } return; } if (data.user) setUser(data.user); const checkIn = data.checkIn || {}; setCheckInReward(Number(checkIn.rewardCoins) || 1); setCheckInToday(Boolean(checkIn.checkedInToday)); setCheckInMessage( data.message || (data.alreadyCheckedIn ? "今日已签到" : `签到成功,获得 ${data.awardedCoins ?? data.rewardCoins ?? 0} 萌芽币`) ); } catch (err) { setCheckInError(err.message); } finally { setCheckInLoading(false); } }; const openOAuthLogin = (provider) => { const ret = getOAuthReturnTo(); if (!ret) return; if (turnstileEnabled) { const tok = mode === "register" ? registerTurnstileToken : loginTurnstileToken; if (!tok) { if (mode === "register") { setRegisterError("请先完成人机验证后再使用第三方登录"); } else { setError("请先完成人机验证后再使用第三方登录"); } return; } const u = new URL(`${API_BASE}/api/auth/oauth/${encodeURIComponent(provider)}/start`); u.searchParams.set("return_to", ret); u.searchParams.set("turnstile_token", tok); window.location.href = u.toString(); return; } window.location.href = `${API_BASE}/api/auth/oauth/${provider}/start?return_to=${encodeURIComponent(ret)}`; }; const bindOAuth = async (provider) => { const t = localStorage.getItem("sproutgate_token"); if (!t) return; setProfileError(""); setProfileMessage(""); try { const res = await fetch(`${API_BASE}/api/auth/oauth/${provider}/bind`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${t}`, ...authClientFetchHeaders() }, body: JSON.stringify({ returnTo: getOAuthReturnTo() }) }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "无法开始绑定"); if (data.url) window.location.href = data.url; } catch (e) { setProfileError(e.message || "绑定失败"); } }; const unlinkOAuth = async (provider) => { if (!window.confirm("确定要解绑该第三方账号吗?")) return; const t = localStorage.getItem("sproutgate_token"); if (!t) return; setProfileError(""); setProfileMessage(""); try { const res = await fetch(`${API_BASE}/api/auth/oauth/${provider}`, { method: "DELETE", headers: { Authorization: `Bearer ${t}`, ...authClientFetchHeaders() } }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "解绑失败"); if (data.user) setUser(data.user); setProfileMessage("已更新第三方账号绑定"); } catch (e) { setProfileError(e.message || "解绑失败"); } }; if (oauthStandalone && !authFlow?.redirectUri?.trim()) { return ( 无效授权链接 缺少回跳地址(redirect_uri 或 return_url)。请从第三方应用重新发起登录。 ); } const showOAuthConsent = oauthStandalone && isAuthFlow && Boolean(user); const showProfile = Boolean(user) && !showOAuthConsent; return ( {showOAuthConsent && ( )} {showProfile && ( )} ); }
缺少回跳地址(redirect_uri 或 return_url)。请从第三方应用重新发起登录。