feat: 更新SproutGate前后端代码
This commit is contained in:
@@ -6,12 +6,15 @@ import {
|
||||
clearAuthClientContext,
|
||||
fetchClientVisitMeta,
|
||||
formatAuthBanMessage,
|
||||
persistAuthClientFromFlow
|
||||
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 }) {
|
||||
export default function UserPortal({ onReady, authFlow, onPreviewImage, oauthStandalone = false }) {
|
||||
const [account, setAccount] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [user, setUser] = useState(null);
|
||||
@@ -23,6 +26,9 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage }) {
|
||||
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);
|
||||
@@ -79,6 +85,9 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage }) {
|
||||
localStorage.removeItem("sproutgate_token");
|
||||
localStorage.removeItem("sproutgate_token_expires_at");
|
||||
setUser(null);
|
||||
setProfileForm({
|
||||
username: "", phone: "", avatarUrl: "", websiteUrl: "", bio: "", password: ""
|
||||
});
|
||||
};
|
||||
|
||||
const bearerHeaders = (tokenValue) => ({
|
||||
@@ -160,6 +169,12 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage }) {
|
||||
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);
|
||||
}
|
||||
} catch {
|
||||
/* 忽略,默认不要求邀请码 */
|
||||
@@ -168,18 +183,18 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage }) {
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
// 仅在外部 user 更新且资料弹窗关闭时同步表单,避免 /api/auth/me 二次请求在编辑期间把表单项覆盖回服务器值(QQ 用户会把已填写的自定义头像链接清空)
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setProfileForm({
|
||||
username: user.username || "",
|
||||
phone: user.phone || "",
|
||||
avatarUrl: user.avatarUrl || "",
|
||||
websiteUrl: user.websiteUrl || "",
|
||||
bio: user.bio || "",
|
||||
password: ""
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
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();
|
||||
@@ -216,7 +231,7 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage }) {
|
||||
syncCurrentUser(data.token).catch(() => {});
|
||||
setAccount("");
|
||||
setPassword("");
|
||||
if (isAuthFlow) {
|
||||
if (isAuthFlow && !oauthStandalone) {
|
||||
redirectToAuthCallback(data.token, data.user, data.expiresAt || "");
|
||||
return;
|
||||
}
|
||||
@@ -228,10 +243,8 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage }) {
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem("sproutgate_token");
|
||||
localStorage.removeItem("sproutgate_token_expires_at");
|
||||
clearAuthClientContext();
|
||||
setUser(null);
|
||||
clearTokenAndUser();
|
||||
setProfileEditorOpen(false);
|
||||
setProfileEditorFocus("");
|
||||
setSecondaryEditorOpen(false);
|
||||
@@ -262,13 +275,23 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage }) {
|
||||
};
|
||||
|
||||
const handleRegisterChange = (field, value) => {
|
||||
if (field === "account") {
|
||||
value = normalizeSelfServiceAccountInput(value, registrationAccountMax);
|
||||
}
|
||||
setRegisterForm((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const resetRegisterFlow = () => {
|
||||
setRegisterSent(false);
|
||||
setRegisterExpiresAt("");
|
||||
setRegisterForm({ account: "", password: "", username: "", email: "", code: "", inviteCode: "" });
|
||||
setRegisterForm({
|
||||
account: randomSelfServiceAccount(10),
|
||||
password: "",
|
||||
username: "",
|
||||
email: "",
|
||||
code: "",
|
||||
inviteCode: ""
|
||||
});
|
||||
};
|
||||
|
||||
const handleSendCode = async (event) => {
|
||||
@@ -281,6 +304,11 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage }) {
|
||||
setRegisterLoading(false);
|
||||
return;
|
||||
}
|
||||
if (registerForm.account.length < registrationAccountMin) {
|
||||
setRegisterError(`账户至少 ${registrationAccountMin} 位,仅小写字母与数字`);
|
||||
setRegisterLoading(false);
|
||||
return;
|
||||
}
|
||||
if (registrationRequireInvite && !String(registerForm.inviteCode || "").trim()) {
|
||||
setRegisterError("请输入邀请码");
|
||||
setRegisterLoading(false);
|
||||
@@ -330,7 +358,12 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage }) {
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "验证失败");
|
||||
setRegisterMessage("注册成功,请使用账号登录");
|
||||
const coins = Number(data.user?.sproutCoins);
|
||||
setRegisterMessage(
|
||||
Number.isFinite(coins) && coins > 0
|
||||
? `注册成功,已获得 ${coins} 萌芽币,请使用账号登录`
|
||||
: "注册成功,请使用账号登录"
|
||||
);
|
||||
setRegisterSent(false);
|
||||
setRegisterForm({ account: "", password: "", username: "", email: "", code: "", inviteCode: "" });
|
||||
setMode("login");
|
||||
@@ -542,6 +575,16 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage }) {
|
||||
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);
|
||||
};
|
||||
@@ -613,14 +656,26 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage }) {
|
||||
}
|
||||
};
|
||||
|
||||
if (oauthStandalone && !authFlow?.redirectUri?.trim()) {
|
||||
return (
|
||||
<section className="panel">
|
||||
<div className="card form oauth-authorize-invalid">
|
||||
<h2>无效授权链接</h2>
|
||||
<p className="hint">缺少回跳地址(redirect_uri 或 return_url)。请从第三方应用重新发起登录。</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const showOAuthConsent = oauthStandalone && isAuthFlow && Boolean(user);
|
||||
const showProfile = Boolean(user) && !showOAuthConsent;
|
||||
|
||||
return (
|
||||
<section className="panel">
|
||||
<UserPortalAuthSection
|
||||
isAuthFlow={isAuthFlow}
|
||||
oauthStandalone={oauthStandalone}
|
||||
user={user}
|
||||
onPreviewImage={onPreviewImage}
|
||||
handleContinueAuth={handleContinueAuth}
|
||||
handleSwitchAuthAccount={handleSwitchAuthAccount}
|
||||
mode={mode}
|
||||
setMode={setMode}
|
||||
account={account}
|
||||
@@ -643,6 +698,9 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage }) {
|
||||
setRegisterError={setRegisterError}
|
||||
setRegisterMessage={setRegisterMessage}
|
||||
registrationRequireInvite={registrationRequireInvite}
|
||||
inviteRegisterReward={inviteRegisterReward}
|
||||
registrationAccountMin={registrationAccountMin}
|
||||
registrationAccountMax={registrationAccountMax}
|
||||
resetRegisterFlow={resetRegisterFlow}
|
||||
resetForm={resetForm}
|
||||
handleResetChange={handleResetChange}
|
||||
@@ -656,7 +714,16 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage }) {
|
||||
setResetError={setResetError}
|
||||
setResetMessage={setResetMessage}
|
||||
/>
|
||||
{user && (
|
||||
{showOAuthConsent && (
|
||||
<OAuthConsentScreen
|
||||
authFlow={authFlow}
|
||||
user={user}
|
||||
onAllow={handleContinueAuth}
|
||||
onSwitchAccount={handleSwitchAuthAccount}
|
||||
onPreviewImage={onPreviewImage}
|
||||
/>
|
||||
)}
|
||||
{showProfile && (
|
||||
<UserPortalProfileSection
|
||||
user={user}
|
||||
onPreviewImage={onPreviewImage}
|
||||
|
||||
Reference in New Issue
Block a user