refactor: centralize Logto config and improve auth flow

- Centralized Logto domain and API base URLs in `common.ts` to avoid duplication.
- Replaced deprecated `Modal.useModal` with `App.useApp().modal` for consistent Ant Design usage.
- Refactored `useAutoLogin` hook to handle token expiration checks and trigger re-authentication.
- Updated `UserCenter` and `FavPlayersPage` to use the new `autoSignIn` flow.
- Removed the `useAuthHeaders` hook as logic was consolidated into `useAutoLogin`.
- Added `AUTH_CALLBACK_URL` and `USER_CENTER_URL` constants for cleaner routing.
This commit is contained in:
kyuuseiryuu 2026-03-16 13:38:02 +09:00
parent de05ca2ecf
commit f188b4eac4
9 changed files with 92 additions and 56 deletions

View File

@ -7,17 +7,18 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ConfigProvider, Empty, theme } from "antd";
import { App, ConfigProvider, Empty, theme } from "antd";
import zhCN from 'antd/locale/zh_CN';
import { RouterProvider } from "react-router";
import { LogtoProvider, type LogtoConfig } from '@logto/react';
import { route } from "./routes";
import { LOGTO_RESOURCE } from "./utils/constants";
import { LOGTO_DOMAIN } from "./utils/common";
const elem = document.getElementById("root")!;
const config: LogtoConfig = {
endpoint: 'https://logto.ksr.la/',
endpoint: LOGTO_DOMAIN,
appId: 'iq0oceaeqazpcned7hzil',
resources: [LOGTO_RESOURCE],
};
@ -28,9 +29,11 @@ const app = (
locale={zhCN}
renderEmpty={() => <Empty description={'暂无数据'} />}
>
<LogtoProvider config={config}>
<RouterProvider router={route} />
</LogtoProvider>
<App>
<LogtoProvider config={config}>
<RouterProvider router={route} />
</LogtoProvider>
</App>
</ConfigProvider>
</StrictMode>
);

View File

@ -1,17 +1,25 @@
import { useLogto } from "@logto/react"
import { useEffect, useState } from "react";
import { LOGTO_RESOURCE } from "../utils/constants";
import { App, Button } from "antd";
import useAutoLogin from "./useAutoLogin";
export const useAuthHeaders = (): HeadersInit => {
const { isAuthenticated, getAccessToken } = useLogto();
const { login } = useAutoLogin();
const [headers, setHeaders] = useState<HeadersInit>({});
const { autoSignIn } = useAutoLogin();
const app = App.useApp();
useEffect(() => {
if (isAuthenticated) {
getAccessToken(LOGTO_RESOURCE).then(token => {
if (!token) {
login();
app.notification.warning({
key: 'use-auth-headers-login-expired',
title: '登陆已过期',
actions: [
<Button onClick={() => autoSignIn() }></Button>
]
})
return;
}
setHeaders({ Authorization: `Bearer ${token}` })

View File

@ -1,15 +1,24 @@
import { useCallback } from "react"
import { useNavigate } from "react-router";
import { useLogto } from "@logto/react";
import { useCallback, useEffect, useState } from "react"
import { AUTH_CALLBACK_URL } from "../utils/front";
const useAutoLogin = () => {
const navigate = useNavigate();
const login = useCallback((redirect = window.location.pathname) => {
if (redirect) {
sessionStorage.setItem('redirect', redirect);
}
navigate('/user-center?autoSignIn=true');
const { isAuthenticated, getAccessToken } = useLogto();
const [isAuthExpired, setIsAuthExpired] = useState(false);
const { signIn } = useLogto();
const autoSignIn = useCallback((redirect?: string) => {
sessionStorage.setItem('redirect', redirect ?? window.location.pathname);
signIn(AUTH_CALLBACK_URL);
}, []);
return { login };
useEffect(() => {
if (isAuthenticated) {
getAccessToken().then(e => {
if (!e) setIsAuthExpired(true); // Assuming
});
}
}, [isAuthenticated]);
return { autoSignIn, isAuthExpired };
}
export default useAutoLogin;

View File

@ -1,4 +1,4 @@
import { Avatar, Button, Card, Divider, Flex, Image, message as AntdMessage, Popconfirm, Radio, Segmented, Spin, Typography } from "antd";
import { Avatar, Button, Card, Divider, Flex, Image, message as AntdMessage, Popconfirm, Radio, Segmented, Spin, Typography, App } from "antd";
import { useFavPlayerStore } from "../store/useFavPlayerStore";
import { useNavigate } from "react-router";
import { useCallback, useEffect, useMemo, useState } from "react";
@ -20,13 +20,12 @@ enum ShowType {
}
export function FavePlayersPage() {
const [message, contextHolder] = AntdMessage.useMessage();
const { favMap, unFav } = useFavPlayerStore(state => state);
const [sortType, setSortType] = useState<SortType>(SortType.DEFAULT);
const [showType, setShowType] = useState(ShowType.LOCAL);
const [claims, setClaims] = useState<IdTokenClaims>();
const headers = useAuthHeaders();
const { login } = useAutoLogin();
const { autoSignIn } = useAutoLogin();
const localList = Object.values(favMap);
const favListRequest = useRequest<XCXProfile[], [string]>(async (aud: string) => {
const res = await fetch(`/api/fav`, { headers });
@ -49,6 +48,7 @@ export function FavePlayersPage() {
}, [favMap, sortType, showType, localList, favListRequest]);
const navigate = useNavigate();
const { isAuthenticated, getIdTokenClaims } = useLogto();
const message = App.useApp().message;
useEffect(() => {
if (!isAuthenticated) return;
const id = setTimeout(async () => {
@ -75,7 +75,6 @@ export function FavePlayersPage() {
}, []);
return (
<div className="app">
{contextHolder}
<Flex vertical gap={48}>
<Typography.Title></Typography.Title>
<Flex justify="center" align="center" gap={12} style={{ display: isAuthenticated ? '' : 'none' }}>
@ -143,7 +142,7 @@ export function FavePlayersPage() {
) }
{ (showType === ShowType.LOCAL && !isAuthenticated && list.length > 0) && (
<Button
onClick={() => login(window.location.pathname)}
onClick={() => autoSignIn()}
icon={<LoginOutlined />}
></Button>
) }

View File

@ -1,8 +1,11 @@
import { EditOutlined, FileTextOutlined, HomeOutlined, KeyOutlined, LockOutlined, LoginOutlined, LogoutOutlined, MailOutlined, MobileOutlined } from "@ant-design/icons";
import { EditOutlined, FileTextOutlined, HomeOutlined, KeyOutlined, LockOutlined, LoginOutlined, LogoutOutlined, MailOutlined, MobileOutlined, ReloadOutlined } from "@ant-design/icons";
import { useLogto, type IdTokenClaims } from "@logto/react";
import { App, Avatar, Button, Divider, Flex, Modal, Typography } from "antd";
import { App, Avatar, Button, Divider, Flex, Typography } from "antd";
import { useCallback, useEffect, useState } from "react";
import { useLocation, useNavigate } from "react-router";
import { AUTH_CALLBACK_URL, USER_CENTER_URL } from "../utils/front";
import useAutoLogin from "../hooks/useAutoLogin";
import { LOGTO_DOMAIN } from "../utils/common";
enum modifyRoutes {
username = '/account/username',
@ -13,17 +16,16 @@ enum modifyRoutes {
backup_code = '/account/backup-codes/generate',
}
const redirect = encodeURIComponent(`${window.location.origin}/user-center`);
const logto = 'https://logto.ksr.la';
const redirect = encodeURIComponent(USER_CENTER_URL);
export const UserCenter = () => {
const { signIn, isAuthenticated, signOut, getIdTokenClaims } = useLogto();
const { isAuthExpired, autoSignIn } = useAutoLogin();
const [user, setUser] = useState<IdTokenClaims>();
const navigate = useNavigate();
const location = useLocation();
const handleSignIn = useCallback(() => {
const redirect = `${window.location.origin}/auth/callback`;
signIn(redirect);
signIn(AUTH_CALLBACK_URL);
}, []);
useEffect(() => {
if (isAuthenticated) return;
@ -37,9 +39,9 @@ export const UserCenter = () => {
getIdTokenClaims().then(claims => setUser(claims));
}, [isAuthenticated]);
const handleModifyInfo = useCallback((url: string) => {
window.location.href = `${logto}${url}?redirect=${redirect}`;
window.location.href = `${LOGTO_DOMAIN}${url}?redirect=${redirect}`;
}, []);
const [modal, contextHolder] = Modal.useModal();
const app = App.useApp();
if (!isAuthenticated) {
return (
<div className="app">
@ -57,7 +59,6 @@ export const UserCenter = () => {
}
return (
<>
{contextHolder}
<Flex className="app" gap={12} vertical align="center" style={{ maxWidth: 600 }}>
<Avatar size={128} src={user?.picture ?? user?.name} />
<Flex>
@ -73,24 +74,35 @@ export const UserCenter = () => {
<Divider />
<Button block type="primary" onClick={() => navigate('/')} icon={<HomeOutlined />}></Button>
<Divider />
<Button
block
danger
icon={<LoginOutlined />}
onClick={() => modal.confirm({
maskClosable: true,
title: '确认登出?',
cancelText: '保持登录',
okText: '确认登出',
okButtonProps: {
icon: <LogoutOutlined />,
danger: true,
},
onOk: () => signOut(`${window.location.origin}/user-center`),
})}
>
</Button>
<Flex align="center" justify="center" style={{ width: '100%' }} gap={12}>
{isAuthExpired && (
<Flex flex={1}>
<Button block icon={<ReloadOutlined />} onClick={() => autoSignIn()}>
</Button>
</Flex>
)}
<Flex flex={1}>
<Button
block
danger
icon={<LoginOutlined />}
onClick={() => app.modal.confirm({
maskClosable: true,
title: '确认登出?',
cancelText: '保持登录',
okText: '确认登出',
okButtonProps: {
icon: <LogoutOutlined />,
danger: true,
},
onOk: () => signOut(USER_CENTER_URL),
})}
>
</Button>
</Flex>
</Flex>
</Flex>
</>
);

View File

@ -1,8 +1,8 @@
import type { GamesData, XCXFindUserResp, XCXMember, XCXProfile, XCXTag } from "../types";
import { BASE_URL } from "../utils/common";
import { KAIQIU_BASE_URL } from "../utils/common";
import { redis } from "../utils/server";
const XCX_BASE_URL = `${BASE_URL}/xcx/public/index.php`;
const XCX_BASE_URL = `${KAIQIU_BASE_URL}/xcx/public/index.php`;
export function createXCXHeader(token: string) {
const xcxDefaultHeaders = {

View File

@ -1,6 +1,7 @@
import { chunk } from "lodash";
export const BASE_URL = `https://kaiqiuwang.cc`;
export const KAIQIU_BASE_URL = `https://kaiqiuwang.cc`;
export const LOGTO_DOMAIN = 'https://logto.ksr.la';
export function sneckGroup(size: number, groupLen: number) {
const indexArray = new Array<number>(size).fill(0).map((_, i) => i);

View File

@ -90,4 +90,8 @@ export const openWebMapRaw = (type: MapType, location: MapLocation): void => {
if (url) {
window.open(url, '_blank');
}
};
};
export const AUTH_CALLBACK_URL = `${window.location.origin}/auth/callback`;
export const USER_CENTER_URL = `${window.location.origin}/user-center`;

View File

@ -1,7 +1,7 @@
import type { Player } from "../types";
import * as cheerio from "cheerio";
import { XCXAPI } from "../services/xcxApi";
import { BASE_URL } from "./common";
import { KAIQIU_BASE_URL, LOGTO_DOMAIN } from "./common";
import { RedisClient } from "bun";
import { createRemoteJWKSet, jwtVerify } from 'jose';
import { LOGTO_RESOURCE } from "./constants";
@ -48,7 +48,7 @@ export const htmlRequestHeaders = {
* @returns HTML
*/
export async function fetchEventContentHTML(matchId: string) {
const url = `${BASE_URL}/home/space.php?do=event&id=${matchId}&view=member&status=2`;
const url = `${KAIQIU_BASE_URL}/home/space.php?do=event&id=${matchId}&view=member&status=2`;
const resp = await fetch(url, { headers: htmlRequestHeaders });
return resp.text() ?? ''
}
@ -97,7 +97,7 @@ export const extractBearerTokenFromHeaders = (authorization: string | null) => {
return authorization.slice(7); // The length of 'Bearer ' is 7
};
const jwks = createRemoteJWKSet(new URL('https://logto.ksr.la/oidc/jwks'));
const jwks = createRemoteJWKSet(new URL(`${LOGTO_DOMAIN}/oidc/jwks`));
export const verifyLogtoToken = async (headers: Headers) => {
const auth = headers.get('Authorization');
@ -110,7 +110,7 @@ export const verifyLogtoToken = async (headers: Headers) => {
jwks,
{
// Expected issuer of the token, issued by the Logto server
issuer: 'https://logto.ksr.la/oidc',
issuer: `${LOGTO_DOMAIN}/oidc`,
// Expected audience token, the resource indicator of the current API
audience: LOGTO_RESOURCE,
}