Update SproutGate
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import {
|
||||
API_BASE,
|
||||
authClientFetchHeaders,
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
clearAuthClientContext,
|
||||
fetchClientVisitMeta,
|
||||
formatAuthBanMessage,
|
||||
getOAuthReturnTo,
|
||||
normalizeSelfServiceAccountInput,
|
||||
persistAuthClientFromFlow,
|
||||
randomSelfServiceAccount
|
||||
@@ -14,7 +15,13 @@ import UserPortalAuthSection from "./userPortal/UserPortalAuthSection";
|
||||
import UserPortalProfileSection from "./userPortal/UserPortalProfileSection";
|
||||
import OAuthConsentScreen from "./OAuthConsentScreen";
|
||||
|
||||
export default function UserPortal({ onReady, authFlow, onPreviewImage, oauthStandalone = false }) {
|
||||
export default function UserPortal({
|
||||
onReady,
|
||||
authFlow,
|
||||
onPreviewImage,
|
||||
oauthStandalone = false,
|
||||
onGuestAuthLayoutChange
|
||||
}) {
|
||||
const [account, setAccount] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [user, setUser] = useState(null);
|
||||
@@ -67,6 +74,22 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage, oauthSta
|
||||
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 = "") => {
|
||||
@@ -148,6 +171,39 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage, oauthSta
|
||||
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());
|
||||
@@ -161,6 +217,14 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage, oauthSta
|
||||
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 () => {
|
||||
@@ -175,6 +239,20 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage, oauthSta
|
||||
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 {
|
||||
/* 忽略,默认不要求邀请码 */
|
||||
@@ -202,6 +280,7 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage, oauthSta
|
||||
setError("");
|
||||
try {
|
||||
const loginPayload = { account, password };
|
||||
if (turnstileEnabled) loginPayload.turnstileToken = loginTurnstileToken;
|
||||
const cid = (authFlow?.clientId || "").trim();
|
||||
const cname = (authFlow?.clientName || "").trim();
|
||||
if (cid) {
|
||||
@@ -237,6 +316,8 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage, oauthSta
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
setLoginTurnstileToken("");
|
||||
loginTurnstileRef.current?.reset();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -321,6 +402,7 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage, oauthSta
|
||||
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`, {
|
||||
@@ -335,6 +417,8 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage, oauthSta
|
||||
setRegisterMessage("验证码已发送,请检查邮箱");
|
||||
} catch (err) {
|
||||
setRegisterError(err.message);
|
||||
setRegisterTurnstileToken("");
|
||||
registerTurnstileRef.current?.reset();
|
||||
} finally {
|
||||
setRegisterLoading(false);
|
||||
}
|
||||
@@ -656,6 +740,71 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage, oauthSta
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<section className="panel">
|
||||
@@ -713,6 +862,21 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage, oauthSta
|
||||
handleResetPassword={handleResetPassword}
|
||||
setResetError={setResetError}
|
||||
setResetMessage={setResetMessage}
|
||||
githubOAuthEnabled={githubOAuthEnabled}
|
||||
giteaOAuthEnabled={giteaOAuthEnabled}
|
||||
googleOAuthEnabled={googleOAuthEnabled}
|
||||
microsoftOAuthEnabled={microsoftOAuthEnabled}
|
||||
linuxdoOAuthEnabled={linuxdoOAuthEnabled}
|
||||
oauthLogos={oauthLogos}
|
||||
onOAuthLogin={openOAuthLogin}
|
||||
turnstileEnabled={turnstileEnabled}
|
||||
turnstileSiteKey={turnstileSiteKey}
|
||||
loginTurnstileToken={loginTurnstileToken}
|
||||
onLoginTurnstileToken={setLoginTurnstileToken}
|
||||
loginTurnstileRef={loginTurnstileRef}
|
||||
registerTurnstileToken={registerTurnstileToken}
|
||||
onRegisterTurnstileToken={setRegisterTurnstileToken}
|
||||
registerTurnstileRef={registerTurnstileRef}
|
||||
/>
|
||||
{showOAuthConsent && (
|
||||
<OAuthConsentScreen
|
||||
@@ -756,6 +920,14 @@ export default function UserPortal({ onReady, authFlow, onPreviewImage, oauthSta
|
||||
checkInError={checkInError}
|
||||
checkInMessage={checkInMessage}
|
||||
handleCheckIn={handleCheckIn}
|
||||
githubOAuthEnabled={githubOAuthEnabled}
|
||||
giteaOAuthEnabled={giteaOAuthEnabled}
|
||||
googleOAuthEnabled={googleOAuthEnabled}
|
||||
microsoftOAuthEnabled={microsoftOAuthEnabled}
|
||||
linuxdoOAuthEnabled={linuxdoOAuthEnabled}
|
||||
oauthLogos={oauthLogos}
|
||||
onBindOAuth={bindOAuth}
|
||||
onUnlinkOAuth={unlinkOAuth}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user