872 lines
28 KiB
TypeScript
872 lines
28 KiB
TypeScript
import {
|
|
type ReactNode,
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
|
|
import { useGameSettings } from '../../hooks/useGameSettings';
|
|
import {
|
|
AUTH_STATE_EVENT,
|
|
refreshStoredAccessToken,
|
|
} from '../../services/apiClient';
|
|
import {
|
|
type AuthAuditLogEntry,
|
|
type AuthCaptchaChallenge,
|
|
authEntry,
|
|
type AuthLoginMethod,
|
|
type AuthRiskBlockSummary,
|
|
type AuthSessionSummary,
|
|
type AuthUser,
|
|
bindWechatPhone,
|
|
changePassword,
|
|
changePhoneNumber,
|
|
consumeAuthCallbackResult,
|
|
getAuthAuditLogs,
|
|
getAuthLoginOptions,
|
|
getAuthRiskBlocks,
|
|
getAuthSessions,
|
|
getCaptchaChallengeFromError,
|
|
getCurrentAuthUser,
|
|
liftAuthRiskBlock,
|
|
loginWithPhoneCode,
|
|
logoutAllAuthSessions,
|
|
logoutAuthUser,
|
|
redeemRegistrationInviteCode,
|
|
resetPassword,
|
|
revokeAuthSession,
|
|
sendPhoneLoginCode,
|
|
setStoredLastLoginPhone,
|
|
startWechatLogin,
|
|
} from '../../services/authService';
|
|
import { AccountModal } from './AccountModal';
|
|
import { AuthUiContext, type PlatformSettingsSection } from './AuthUiContext';
|
|
import { BindPhoneScreen } from './BindPhoneScreen';
|
|
import { LoginScreen } from './LoginScreen';
|
|
import { RegistrationInviteModal } from './RegistrationInviteModal';
|
|
|
|
type AuthGateProps = {
|
|
children: ReactNode;
|
|
};
|
|
|
|
type AuthStatus =
|
|
| 'checking'
|
|
| 'recovering'
|
|
| 'unauthenticated'
|
|
| 'pending_bind_phone'
|
|
| 'ready'
|
|
| 'error';
|
|
|
|
const FALLBACK_LOGIN_METHODS: AuthLoginMethod[] = ['password'];
|
|
|
|
function readInviteCodeFromLocation(): string {
|
|
const params = new URLSearchParams(window.location.search || '');
|
|
return (params.get('inviteCode') || params.get('invite_code') || '')
|
|
.trim()
|
|
.replace(/[^0-9a-z]/gi, '')
|
|
.toUpperCase();
|
|
}
|
|
|
|
function normalizeAvailableLoginMethods(
|
|
methods: AuthLoginMethod[] | null | undefined,
|
|
): AuthLoginMethod[] {
|
|
const normalizedMethods = Array.from(new Set(methods ?? []));
|
|
|
|
// 密码登录由 Rust auth entry 固定承载,不依赖短信或微信环境开关。
|
|
// 当 login-options 联调失败或配置返回空数组时,仍要保留账号入口,避免登录弹窗失去可操作方式。
|
|
return normalizedMethods.length > 0
|
|
? normalizedMethods
|
|
: FALLBACK_LOGIN_METHODS;
|
|
}
|
|
|
|
export function AuthGate({ children }: AuthGateProps) {
|
|
const [status, setStatus] = useState<AuthStatus>('checking');
|
|
const [user, setUser] = useState<AuthUser | null>(null);
|
|
const [availableLoginMethods, setAvailableLoginMethods] = useState<
|
|
AuthLoginMethod[]
|
|
>([]);
|
|
const [error, setError] = useState('');
|
|
const [sendingCode, setSendingCode] = useState(false);
|
|
const [loggingIn, setLoggingIn] = useState(false);
|
|
const [bindingPhone, setBindingPhone] = useState(false);
|
|
const [wechatLoading, setWechatLoading] = useState(false);
|
|
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
const [pendingInviteCode, setPendingInviteCode] = useState('');
|
|
const [showRegistrationInviteModal, setShowRegistrationInviteModal] =
|
|
useState(false);
|
|
const [submittingRegistrationInvite, setSubmittingRegistrationInvite] =
|
|
useState(false);
|
|
const [registrationInviteError, setRegistrationInviteError] = useState('');
|
|
const [showSettingsModal, setShowSettingsModal] = useState(false);
|
|
const [settingsEntryMode, setSettingsEntryMode] = useState<
|
|
'settings' | 'account'
|
|
>('settings');
|
|
const [initialSettingsSection, setInitialSettingsSection] =
|
|
useState<PlatformSettingsSection | null>(null);
|
|
const [sessions, setSessions] = useState<AuthSessionSummary[]>([]);
|
|
const [loadingSessions, setLoadingSessions] = useState(false);
|
|
const [auditLogs, setAuditLogs] = useState<AuthAuditLogEntry[]>([]);
|
|
const [loadingAuditLogs, setLoadingAuditLogs] = useState(false);
|
|
const [riskBlocks, setRiskBlocks] = useState<AuthRiskBlockSummary[]>([]);
|
|
const [loadingRiskBlocks, setLoadingRiskBlocks] = useState(false);
|
|
const [loginCaptchaChallenge, setLoginCaptchaChallenge] =
|
|
useState<AuthCaptchaChallenge | null>(null);
|
|
const [bindCaptchaChallenge, setBindCaptchaChallenge] =
|
|
useState<AuthCaptchaChallenge | null>(null);
|
|
const [changePhoneCaptchaChallenge, setChangePhoneCaptchaChallenge] =
|
|
useState<AuthCaptchaChallenge | null>(null);
|
|
const pendingProtectedActionRef = useRef<(() => void) | null>(null);
|
|
const autoOpenedInviteCodeRef = useRef<string | null>(null);
|
|
const hasRenderedPlatformContentRef = useRef(false);
|
|
const canKeepPlatformContentMounted =
|
|
hasRenderedPlatformContentRef.current &&
|
|
(status === 'checking' || status === 'recovering');
|
|
const readyUser =
|
|
status === 'ready' || canKeepPlatformContentMounted ? user : null;
|
|
const settings = useGameSettings(readyUser?.id ?? null);
|
|
const platformThemeClass = `platform-theme--${settings.platformTheme}`;
|
|
|
|
if (status === 'ready' || status === 'unauthenticated') {
|
|
hasRenderedPlatformContentRef.current = true;
|
|
}
|
|
|
|
const activateReadyUser = useCallback((nextUser: AuthUser) => {
|
|
// 受保护业务 hook 只在 readyUser 暴露后启动,必须先保证请求层能带 Bearer token。
|
|
setUser(nextUser);
|
|
setStatus('ready');
|
|
}, []);
|
|
|
|
const clearLocalAuthenticatedState = useCallback(() => {
|
|
// 退出动作必须先收回前端鉴权上下文,再等待后端吊销完成。
|
|
// 否则平台壳层会在无刷新状态下继续暴露旧用户的私有作品缓存。
|
|
pendingProtectedActionRef.current = null;
|
|
setUser(null);
|
|
setStatus('unauthenticated');
|
|
setShowLoginModal(false);
|
|
setShowRegistrationInviteModal(false);
|
|
setShowSettingsModal(false);
|
|
setSettingsEntryMode('settings');
|
|
setInitialSettingsSection(null);
|
|
setSessions([]);
|
|
setAuditLogs([]);
|
|
setRiskBlocks([]);
|
|
setLoginCaptchaChallenge(null);
|
|
setBindCaptchaChallenge(null);
|
|
setChangePhoneCaptchaChallenge(null);
|
|
setPendingInviteCode('');
|
|
setRegistrationInviteError('');
|
|
setError('');
|
|
}, []);
|
|
|
|
const logoutCurrentSession = useCallback(async () => {
|
|
clearLocalAuthenticatedState();
|
|
try {
|
|
await logoutAuthUser();
|
|
} catch (logoutError) {
|
|
setError(
|
|
logoutError instanceof Error
|
|
? logoutError.message
|
|
: '退出登录失败,请刷新页面确认状态。',
|
|
);
|
|
}
|
|
}, [clearLocalAuthenticatedState]);
|
|
|
|
const logoutAllSessions = useCallback(async () => {
|
|
clearLocalAuthenticatedState();
|
|
try {
|
|
await logoutAllAuthSessions();
|
|
} catch (logoutError) {
|
|
setError(
|
|
logoutError instanceof Error
|
|
? logoutError.message
|
|
: '退出全部设备失败,请刷新页面确认状态。',
|
|
);
|
|
}
|
|
}, [clearLocalAuthenticatedState]);
|
|
|
|
const closeLoginModal = useCallback(() => {
|
|
pendingProtectedActionRef.current = null;
|
|
setShowLoginModal(false);
|
|
setLoginCaptchaChallenge(null);
|
|
setError('');
|
|
}, []);
|
|
|
|
const closeRegistrationInviteModal = useCallback(() => {
|
|
setShowRegistrationInviteModal(false);
|
|
setRegistrationInviteError('');
|
|
setPendingInviteCode('');
|
|
}, []);
|
|
|
|
const closeSettingsModal = useCallback(() => {
|
|
setShowSettingsModal(false);
|
|
setSettingsEntryMode('settings');
|
|
setInitialSettingsSection(null);
|
|
}, []);
|
|
|
|
const openLoginModal = useCallback(
|
|
(postLoginAction?: (() => void) | null) => {
|
|
if (readyUser) {
|
|
postLoginAction?.();
|
|
return;
|
|
}
|
|
|
|
pendingProtectedActionRef.current = postLoginAction ?? null;
|
|
setShowLoginModal(true);
|
|
},
|
|
[readyUser],
|
|
);
|
|
|
|
const requireAuth = useCallback(
|
|
(action: () => void) => {
|
|
openLoginModal(action);
|
|
},
|
|
[openLoginModal],
|
|
);
|
|
|
|
const openSettingsModal = useCallback(
|
|
(section?: PlatformSettingsSection) => {
|
|
if (readyUser) {
|
|
setSettingsEntryMode('settings');
|
|
setInitialSettingsSection(section ?? null);
|
|
setShowSettingsModal(true);
|
|
return;
|
|
}
|
|
|
|
openLoginModal();
|
|
},
|
|
[openLoginModal, readyUser],
|
|
);
|
|
|
|
const openAccountModal = useCallback(() => {
|
|
if (readyUser) {
|
|
setSettingsEntryMode('account');
|
|
setInitialSettingsSection('account');
|
|
setShowSettingsModal(true);
|
|
return;
|
|
}
|
|
|
|
openLoginModal();
|
|
}, [openLoginModal, readyUser]);
|
|
|
|
useEffect(() => {
|
|
if (status !== 'unauthenticated' || readyUser || showLoginModal) {
|
|
return;
|
|
}
|
|
const inviteCode = readInviteCodeFromLocation();
|
|
if (!inviteCode) {
|
|
return;
|
|
}
|
|
if (autoOpenedInviteCodeRef.current === inviteCode) {
|
|
return;
|
|
}
|
|
autoOpenedInviteCodeRef.current = inviteCode;
|
|
setPendingInviteCode(inviteCode);
|
|
}, [readyUser, showLoginModal, status]);
|
|
|
|
useEffect(() => {
|
|
let isActive = true;
|
|
|
|
const hydrate = async () => {
|
|
const callbackResult = consumeAuthCallbackResult();
|
|
const loadLoginOptions = async () => {
|
|
const options = await getAuthLoginOptions();
|
|
if (!isActive) {
|
|
return null;
|
|
}
|
|
|
|
setAvailableLoginMethods(
|
|
normalizeAvailableLoginMethods(options.availableLoginMethods),
|
|
);
|
|
return options;
|
|
};
|
|
|
|
const resolveGuestFallback = async () => {
|
|
try {
|
|
await loadLoginOptions();
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
|
|
setUser(null);
|
|
setStatus('unauthenticated');
|
|
} catch (optionsError) {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
|
|
setAvailableLoginMethods(FALLBACK_LOGIN_METHODS);
|
|
setUser(null);
|
|
// 中文注释:登录方式接口失败时按产品约定保留密码登录入口;
|
|
// 这里不展示接口读取错误,避免用户误以为登录本身不可用。
|
|
setError(callbackResult?.error ?? '');
|
|
setStatus('unauthenticated');
|
|
}
|
|
};
|
|
|
|
if (callbackResult?.error && isActive) {
|
|
setError(callbackResult.error);
|
|
setShowLoginModal(true);
|
|
}
|
|
|
|
try {
|
|
// 中文注释:打开已登录页面也要主动轮换 refresh cookie。
|
|
// 后端只在 refresh/session 成功续期时写每日登录埋点;如果本地 access token 尚未过期,
|
|
// 仅调用 /auth/me 不会进入续期链路,导致“打开网页”没有登录埋点。
|
|
await refreshStoredAccessToken();
|
|
const nextSession = await getCurrentAuthUser();
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
|
|
if (!nextSession.user) {
|
|
setAvailableLoginMethods(
|
|
normalizeAvailableLoginMethods(nextSession.availableLoginMethods),
|
|
);
|
|
await resolveGuestFallback();
|
|
return;
|
|
}
|
|
|
|
setUser(nextSession.user);
|
|
setAvailableLoginMethods(
|
|
normalizeAvailableLoginMethods(nextSession.availableLoginMethods),
|
|
);
|
|
setStatus(
|
|
nextSession.user.bindingStatus === 'pending_bind_phone'
|
|
? 'pending_bind_phone'
|
|
: 'ready',
|
|
);
|
|
setError(callbackResult?.error ?? '');
|
|
} catch {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
|
|
await resolveGuestFallback();
|
|
}
|
|
};
|
|
|
|
void hydrate();
|
|
|
|
const handleAuthStateChange = () => {
|
|
setStatus('checking');
|
|
void hydrate();
|
|
};
|
|
|
|
window.addEventListener(AUTH_STATE_EVENT, handleAuthStateChange);
|
|
|
|
return () => {
|
|
isActive = false;
|
|
window.removeEventListener(AUTH_STATE_EVENT, handleAuthStateChange);
|
|
};
|
|
}, [activateReadyUser]);
|
|
|
|
useEffect(() => {
|
|
if (!readyUser) {
|
|
setShowSettingsModal(false);
|
|
return;
|
|
}
|
|
|
|
setShowLoginModal(false);
|
|
|
|
const pendingAction = pendingProtectedActionRef.current;
|
|
pendingProtectedActionRef.current = null;
|
|
pendingAction?.();
|
|
}, [readyUser]);
|
|
|
|
useEffect(() => {
|
|
if (!showSettingsModal || status !== 'ready') {
|
|
return;
|
|
}
|
|
|
|
let isActive = true;
|
|
setLoadingRiskBlocks(true);
|
|
setLoadingSessions(true);
|
|
setLoadingAuditLogs(true);
|
|
void getAuthRiskBlocks()
|
|
.then((nextBlocks) => {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
setRiskBlocks(nextBlocks);
|
|
})
|
|
.catch((blockError) => {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
setError(
|
|
blockError instanceof Error
|
|
? blockError.message
|
|
: '读取安全状态失败,请稍后再试。',
|
|
);
|
|
})
|
|
.finally(() => {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
setLoadingRiskBlocks(false);
|
|
});
|
|
void getAuthSessions()
|
|
.then((nextSessions) => {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
setSessions(nextSessions);
|
|
})
|
|
.catch((sessionError) => {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
setError(
|
|
sessionError instanceof Error
|
|
? sessionError.message
|
|
: '读取登录设备失败,请稍后再试。',
|
|
);
|
|
})
|
|
.finally(() => {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
setLoadingSessions(false);
|
|
});
|
|
|
|
void getAuthAuditLogs()
|
|
.then((nextLogs) => {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
setAuditLogs(nextLogs);
|
|
})
|
|
.catch((auditError) => {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
setError(
|
|
auditError instanceof Error
|
|
? auditError.message
|
|
: '读取账号操作记录失败,请稍后再试。',
|
|
);
|
|
})
|
|
.finally(() => {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
setLoadingAuditLogs(false);
|
|
});
|
|
|
|
return () => {
|
|
isActive = false;
|
|
};
|
|
}, [showSettingsModal, status]);
|
|
|
|
const authUiValue = useMemo(
|
|
() => ({
|
|
user: readyUser,
|
|
// 平台内容在 checking/recovering 阶段可以继续挂载,避免闪烁;
|
|
// 但受保护请求只能在真实 ready 且存在用户时再启动。
|
|
canAccessProtectedData: status === 'ready' && Boolean(readyUser),
|
|
openLoginModal,
|
|
requireAuth,
|
|
openSettingsModal,
|
|
openAccountModal,
|
|
setCurrentUser: setUser,
|
|
logout: logoutCurrentSession,
|
|
musicVolume: settings.musicVolume,
|
|
setMusicVolume: settings.setMusicVolume,
|
|
platformTheme: settings.platformTheme,
|
|
setPlatformTheme: settings.setPlatformTheme,
|
|
isHydratingSettings: settings.isHydratingSettings,
|
|
isPersistingSettings: settings.isPersistingSettings,
|
|
settingsError: settings.settingsError,
|
|
}),
|
|
[
|
|
openAccountModal,
|
|
openLoginModal,
|
|
openSettingsModal,
|
|
readyUser,
|
|
requireAuth,
|
|
logoutCurrentSession,
|
|
status,
|
|
settings.isHydratingSettings,
|
|
settings.isPersistingSettings,
|
|
settings.musicVolume,
|
|
settings.platformTheme,
|
|
settings.setMusicVolume,
|
|
settings.setPlatformTheme,
|
|
settings.settingsError,
|
|
],
|
|
);
|
|
|
|
if (status === 'checking' && !canKeepPlatformContentMounted) {
|
|
return (
|
|
<div
|
|
className={`platform-theme ${platformThemeClass} flex min-h-screen items-center justify-center bg-[var(--platform-body-fill)] text-sm text-[var(--platform-text-base)]`}
|
|
>
|
|
正在校验登录状态...
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (status === 'recovering' && !canKeepPlatformContentMounted) {
|
|
return (
|
|
<div
|
|
className={`platform-theme ${platformThemeClass} flex min-h-screen items-center justify-center bg-[var(--platform-body-fill)] text-sm text-[var(--platform-text-base)]`}
|
|
>
|
|
正在恢复登录状态...
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (status === 'pending_bind_phone' && user) {
|
|
return (
|
|
<BindPhoneScreen
|
|
user={user}
|
|
platformTheme={settings.platformTheme}
|
|
sendingCode={sendingCode}
|
|
binding={bindingPhone}
|
|
error={error}
|
|
captchaChallenge={bindCaptchaChallenge}
|
|
onSendCode={async (phone, captcha) => {
|
|
setSendingCode(true);
|
|
setError('');
|
|
try {
|
|
const result = await sendPhoneLoginCode(
|
|
phone,
|
|
'bind_phone',
|
|
captcha,
|
|
);
|
|
setBindCaptchaChallenge(null);
|
|
return result;
|
|
} catch (sendError) {
|
|
const captchaChallenge = getCaptchaChallengeFromError(sendError);
|
|
if (captchaChallenge) {
|
|
setBindCaptchaChallenge(captchaChallenge);
|
|
}
|
|
setError(
|
|
sendError instanceof Error
|
|
? sendError.message
|
|
: '发送验证码失败,请稍后再试。',
|
|
);
|
|
throw sendError;
|
|
} finally {
|
|
setSendingCode(false);
|
|
}
|
|
}}
|
|
onSubmit={async (phone, code) => {
|
|
setBindingPhone(true);
|
|
setError('');
|
|
try {
|
|
const nextUser = await bindWechatPhone(phone, code);
|
|
setBindCaptchaChallenge(null);
|
|
activateReadyUser(nextUser);
|
|
} catch (bindError) {
|
|
setError(
|
|
bindError instanceof Error
|
|
? bindError.message
|
|
: '绑定手机号失败,请稍后再试。',
|
|
);
|
|
} finally {
|
|
setBindingPhone(false);
|
|
}
|
|
}}
|
|
onLogout={async () => {
|
|
await logoutCurrentSession();
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (
|
|
status !== 'ready' &&
|
|
status !== 'unauthenticated' &&
|
|
!canKeepPlatformContentMounted
|
|
) {
|
|
return (
|
|
<div
|
|
className={`platform-theme ${platformThemeClass} flex min-h-screen items-center justify-center bg-[var(--platform-body-fill)] px-6 text-[var(--platform-text-base)]`}
|
|
>
|
|
<div className="platform-auth-card max-w-md rounded-3xl px-6 py-7 text-center">
|
|
<div className="text-base font-medium text-[var(--platform-text-strong)]">
|
|
登录状态异常
|
|
</div>
|
|
<div className="mt-3 text-sm leading-6 text-[var(--platform-text-base)]">
|
|
{error || '账号恢复失败,请刷新页面后重试。'}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="platform-button platform-button--primary mt-5"
|
|
onClick={() => {
|
|
window.location.reload();
|
|
}}
|
|
>
|
|
重新尝试
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AuthUiContext.Provider value={authUiValue}>
|
|
<div className="relative">
|
|
<div className={`platform-theme ${platformThemeClass}`}>
|
|
{readyUser ? (
|
|
<AccountModal
|
|
user={readyUser}
|
|
isOpen={showSettingsModal}
|
|
entryMode={settingsEntryMode}
|
|
initialSection={initialSettingsSection}
|
|
platformTheme={settings.platformTheme}
|
|
riskBlocks={riskBlocks}
|
|
sessions={sessions}
|
|
auditLogs={auditLogs}
|
|
loadingRiskBlocks={loadingRiskBlocks}
|
|
loadingSessions={loadingSessions}
|
|
loadingAuditLogs={loadingAuditLogs}
|
|
isHydratingSettings={settings.isHydratingSettings}
|
|
isPersistingSettings={settings.isPersistingSettings}
|
|
settingsError={settings.settingsError}
|
|
onClose={closeSettingsModal}
|
|
onPlatformThemeChange={settings.setPlatformTheme}
|
|
onLogout={logoutCurrentSession}
|
|
onRefreshRiskBlocks={async () => {
|
|
setLoadingRiskBlocks(true);
|
|
try {
|
|
setRiskBlocks(await getAuthRiskBlocks());
|
|
} catch (blockError) {
|
|
setError(
|
|
blockError instanceof Error
|
|
? blockError.message
|
|
: '读取安全状态失败,请稍后再试。',
|
|
);
|
|
} finally {
|
|
setLoadingRiskBlocks(false);
|
|
}
|
|
}}
|
|
onLiftRiskBlock={async (scopeType) => {
|
|
try {
|
|
await liftAuthRiskBlock(scopeType);
|
|
setRiskBlocks(await getAuthRiskBlocks());
|
|
setAuditLogs(await getAuthAuditLogs());
|
|
} catch (liftError) {
|
|
setError(
|
|
liftError instanceof Error
|
|
? liftError.message
|
|
: '解除保护失败,请稍后再试。',
|
|
);
|
|
}
|
|
}}
|
|
onRefreshSessions={async () => {
|
|
setLoadingSessions(true);
|
|
try {
|
|
setSessions(await getAuthSessions());
|
|
} catch (sessionError) {
|
|
setError(
|
|
sessionError instanceof Error
|
|
? sessionError.message
|
|
: '读取登录设备失败,请稍后再试。',
|
|
);
|
|
} finally {
|
|
setLoadingSessions(false);
|
|
}
|
|
}}
|
|
onRefreshAuditLogs={async () => {
|
|
setLoadingAuditLogs(true);
|
|
try {
|
|
setAuditLogs(await getAuthAuditLogs());
|
|
} catch (auditError) {
|
|
setError(
|
|
auditError instanceof Error
|
|
? auditError.message
|
|
: '读取账号操作记录失败,请稍后再试。',
|
|
);
|
|
} finally {
|
|
setLoadingAuditLogs(false);
|
|
}
|
|
}}
|
|
onRevokeSession={async (sessionId) => {
|
|
try {
|
|
await revokeAuthSession(sessionId);
|
|
setSessions((current) =>
|
|
current.filter(
|
|
(session) => session.sessionId !== sessionId,
|
|
),
|
|
);
|
|
setAuditLogs(await getAuthAuditLogs());
|
|
} catch (revokeError) {
|
|
setError(
|
|
revokeError instanceof Error
|
|
? revokeError.message
|
|
: '移除登录设备失败,请稍后再试。',
|
|
);
|
|
}
|
|
}}
|
|
onLogoutAll={logoutAllSessions}
|
|
changePhoneCaptchaChallenge={changePhoneCaptchaChallenge}
|
|
onSendChangePhoneCode={async (phone, captcha) => {
|
|
try {
|
|
const result = await sendPhoneLoginCode(
|
|
phone,
|
|
'change_phone',
|
|
captcha,
|
|
);
|
|
setChangePhoneCaptchaChallenge(null);
|
|
return result;
|
|
} catch (sendError) {
|
|
const captchaChallenge =
|
|
getCaptchaChallengeFromError(sendError);
|
|
if (captchaChallenge) {
|
|
setChangePhoneCaptchaChallenge(captchaChallenge);
|
|
}
|
|
throw sendError;
|
|
}
|
|
}}
|
|
onChangePhone={async (phone, code) => {
|
|
const nextUser = await changePhoneNumber(phone, code);
|
|
setChangePhoneCaptchaChallenge(null);
|
|
setUser(nextUser);
|
|
}}
|
|
onChangePassword={async (currentPassword, newPassword) => {
|
|
const nextUser = await changePassword(
|
|
currentPassword,
|
|
newPassword,
|
|
);
|
|
setUser(nextUser);
|
|
}}
|
|
/>
|
|
) : null}
|
|
<LoginScreen
|
|
isOpen={showLoginModal}
|
|
platformTheme={settings.platformTheme}
|
|
availableLoginMethods={availableLoginMethods}
|
|
sendingCode={sendingCode}
|
|
loggingIn={loggingIn}
|
|
wechatLoading={wechatLoading}
|
|
error={error}
|
|
captchaChallenge={loginCaptchaChallenge}
|
|
onClose={closeLoginModal}
|
|
onSendCode={async (phone, scene, captcha) => {
|
|
setSendingCode(true);
|
|
setError('');
|
|
try {
|
|
const result = await sendPhoneLoginCode(phone, scene, captcha);
|
|
setLoginCaptchaChallenge(null);
|
|
return result;
|
|
} catch (sendError) {
|
|
const captchaChallenge =
|
|
getCaptchaChallengeFromError(sendError);
|
|
if (captchaChallenge) {
|
|
setLoginCaptchaChallenge(captchaChallenge);
|
|
}
|
|
setError(
|
|
sendError instanceof Error
|
|
? sendError.message
|
|
: '发送验证码失败,请稍后再试。',
|
|
);
|
|
throw sendError;
|
|
} finally {
|
|
setSendingCode(false);
|
|
}
|
|
}}
|
|
onPhoneSubmit={async (phone, code) => {
|
|
setLoggingIn(true);
|
|
setError('');
|
|
try {
|
|
const response = await loginWithPhoneCode(phone, code);
|
|
setStoredLastLoginPhone(phone);
|
|
setLoginCaptchaChallenge(null);
|
|
setShowRegistrationInviteModal(response.created);
|
|
setRegistrationInviteError('');
|
|
activateReadyUser(response.user);
|
|
} catch (loginError) {
|
|
setError(
|
|
loginError instanceof Error
|
|
? loginError.message
|
|
: '登录失败,请稍后再试。',
|
|
);
|
|
} finally {
|
|
setLoggingIn(false);
|
|
}
|
|
}}
|
|
onPasswordSubmit={async (phone, password) => {
|
|
setLoggingIn(true);
|
|
setError('');
|
|
try {
|
|
const nextUser = await authEntry(phone, password);
|
|
setStoredLastLoginPhone(phone);
|
|
activateReadyUser(nextUser);
|
|
} catch (loginError) {
|
|
setError(
|
|
loginError instanceof Error
|
|
? loginError.message
|
|
: '登录失败,请稍后再试。',
|
|
);
|
|
} finally {
|
|
setLoggingIn(false);
|
|
}
|
|
}}
|
|
onResetPassword={async (phone, code, newPassword) => {
|
|
setLoggingIn(true);
|
|
setError('');
|
|
try {
|
|
const nextUser = await resetPassword(phone, code, newPassword);
|
|
setStoredLastLoginPhone(phone);
|
|
activateReadyUser(nextUser);
|
|
} catch (resetError) {
|
|
setError(
|
|
resetError instanceof Error
|
|
? resetError.message
|
|
: '重置密码失败,请稍后再试。',
|
|
);
|
|
} finally {
|
|
setLoggingIn(false);
|
|
}
|
|
}}
|
|
onStartWechatLogin={async () => {
|
|
setWechatLoading(true);
|
|
setError('');
|
|
try {
|
|
await startWechatLogin();
|
|
} catch (wechatError) {
|
|
setError(
|
|
wechatError instanceof Error
|
|
? wechatError.message
|
|
: '微信登录暂不可用,请稍后再试。',
|
|
);
|
|
} finally {
|
|
setWechatLoading(false);
|
|
}
|
|
}}
|
|
/>
|
|
<RegistrationInviteModal
|
|
isOpen={showRegistrationInviteModal}
|
|
platformTheme={settings.platformTheme}
|
|
initialInviteCode={pendingInviteCode}
|
|
submitting={submittingRegistrationInvite}
|
|
error={registrationInviteError}
|
|
onClose={closeRegistrationInviteModal}
|
|
onSubmit={async (inviteCode) => {
|
|
setSubmittingRegistrationInvite(true);
|
|
setRegistrationInviteError('');
|
|
try {
|
|
await redeemRegistrationInviteCode(inviteCode);
|
|
closeRegistrationInviteModal();
|
|
} catch (inviteError) {
|
|
setRegistrationInviteError(
|
|
inviteError instanceof Error
|
|
? inviteError.message
|
|
: '填写邀请码失败,请稍后再试。',
|
|
);
|
|
} finally {
|
|
setSubmittingRegistrationInvite(false);
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
{children}
|
|
</div>
|
|
</AuthUiContext.Provider>
|
|
);
|
|
}
|