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

View File

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

View File

@ -1,15 +1,24 @@
import { useCallback } from "react" import { useLogto } from "@logto/react";
import { useNavigate } from "react-router"; import { useCallback, useEffect, useState } from "react"
import { AUTH_CALLBACK_URL } from "../utils/front";
const useAutoLogin = () => { const useAutoLogin = () => {
const navigate = useNavigate(); const { isAuthenticated, getAccessToken } = useLogto();
const login = useCallback((redirect = window.location.pathname) => { const [isAuthExpired, setIsAuthExpired] = useState(false);
if (redirect) { const { signIn } = useLogto();
sessionStorage.setItem('redirect', redirect); const autoSignIn = useCallback((redirect?: string) => {
} sessionStorage.setItem('redirect', redirect ?? window.location.pathname);
navigate('/user-center?autoSignIn=true'); signIn(AUTH_CALLBACK_URL);
}, []); }, []);
return { login }; useEffect(() => {
if (isAuthenticated) {
getAccessToken().then(e => {
if (!e) setIsAuthExpired(true); // Assuming
});
}
}, [isAuthenticated]);
return { autoSignIn, isAuthExpired };
} }
export default useAutoLogin; 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 { useFavPlayerStore } from "../store/useFavPlayerStore";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
@ -20,13 +20,12 @@ enum ShowType {
} }
export function FavePlayersPage() { export function FavePlayersPage() {
const [message, contextHolder] = AntdMessage.useMessage();
const { favMap, unFav } = useFavPlayerStore(state => state); const { favMap, unFav } = useFavPlayerStore(state => state);
const [sortType, setSortType] = useState<SortType>(SortType.DEFAULT); const [sortType, setSortType] = useState<SortType>(SortType.DEFAULT);
const [showType, setShowType] = useState(ShowType.LOCAL); const [showType, setShowType] = useState(ShowType.LOCAL);
const [claims, setClaims] = useState<IdTokenClaims>(); const [claims, setClaims] = useState<IdTokenClaims>();
const headers = useAuthHeaders(); const headers = useAuthHeaders();
const { login } = useAutoLogin(); const { autoSignIn } = useAutoLogin();
const localList = Object.values(favMap); const localList = Object.values(favMap);
const favListRequest = useRequest<XCXProfile[], [string]>(async (aud: string) => { const favListRequest = useRequest<XCXProfile[], [string]>(async (aud: string) => {
const res = await fetch(`/api/fav`, { headers }); const res = await fetch(`/api/fav`, { headers });
@ -49,6 +48,7 @@ export function FavePlayersPage() {
}, [favMap, sortType, showType, localList, favListRequest]); }, [favMap, sortType, showType, localList, favListRequest]);
const navigate = useNavigate(); const navigate = useNavigate();
const { isAuthenticated, getIdTokenClaims } = useLogto(); const { isAuthenticated, getIdTokenClaims } = useLogto();
const message = App.useApp().message;
useEffect(() => { useEffect(() => {
if (!isAuthenticated) return; if (!isAuthenticated) return;
const id = setTimeout(async () => { const id = setTimeout(async () => {
@ -75,7 +75,6 @@ export function FavePlayersPage() {
}, []); }, []);
return ( return (
<div className="app"> <div className="app">
{contextHolder}
<Flex vertical gap={48}> <Flex vertical gap={48}>
<Typography.Title></Typography.Title> <Typography.Title></Typography.Title>
<Flex justify="center" align="center" gap={12} style={{ display: isAuthenticated ? '' : 'none' }}> <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) && ( { (showType === ShowType.LOCAL && !isAuthenticated && list.length > 0) && (
<Button <Button
onClick={() => login(window.location.pathname)} onClick={() => autoSignIn()}
icon={<LoginOutlined />} icon={<LoginOutlined />}
></Button> ></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 { 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 { useCallback, useEffect, useState } from "react";
import { useLocation, useNavigate } from "react-router"; 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 { enum modifyRoutes {
username = '/account/username', username = '/account/username',
@ -13,17 +16,16 @@ enum modifyRoutes {
backup_code = '/account/backup-codes/generate', backup_code = '/account/backup-codes/generate',
} }
const redirect = encodeURIComponent(`${window.location.origin}/user-center`); const redirect = encodeURIComponent(USER_CENTER_URL);
const logto = 'https://logto.ksr.la';
export const UserCenter = () => { export const UserCenter = () => {
const { signIn, isAuthenticated, signOut, getIdTokenClaims } = useLogto(); const { signIn, isAuthenticated, signOut, getIdTokenClaims } = useLogto();
const { isAuthExpired, autoSignIn } = useAutoLogin();
const [user, setUser] = useState<IdTokenClaims>(); const [user, setUser] = useState<IdTokenClaims>();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const handleSignIn = useCallback(() => { const handleSignIn = useCallback(() => {
const redirect = `${window.location.origin}/auth/callback`; signIn(AUTH_CALLBACK_URL);
signIn(redirect);
}, []); }, []);
useEffect(() => { useEffect(() => {
if (isAuthenticated) return; if (isAuthenticated) return;
@ -37,9 +39,9 @@ export const UserCenter = () => {
getIdTokenClaims().then(claims => setUser(claims)); getIdTokenClaims().then(claims => setUser(claims));
}, [isAuthenticated]); }, [isAuthenticated]);
const handleModifyInfo = useCallback((url: string) => { 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) { if (!isAuthenticated) {
return ( return (
<div className="app"> <div className="app">
@ -57,7 +59,6 @@ export const UserCenter = () => {
} }
return ( return (
<> <>
{contextHolder}
<Flex className="app" gap={12} vertical align="center" style={{ maxWidth: 600 }}> <Flex className="app" gap={12} vertical align="center" style={{ maxWidth: 600 }}>
<Avatar size={128} src={user?.picture ?? user?.name} /> <Avatar size={128} src={user?.picture ?? user?.name} />
<Flex> <Flex>
@ -73,24 +74,35 @@ export const UserCenter = () => {
<Divider /> <Divider />
<Button block type="primary" onClick={() => navigate('/')} icon={<HomeOutlined />}></Button> <Button block type="primary" onClick={() => navigate('/')} icon={<HomeOutlined />}></Button>
<Divider /> <Divider />
<Button <Flex align="center" justify="center" style={{ width: '100%' }} gap={12}>
block {isAuthExpired && (
danger <Flex flex={1}>
icon={<LoginOutlined />} <Button block icon={<ReloadOutlined />} onClick={() => autoSignIn()}>
onClick={() => modal.confirm({
maskClosable: true, </Button>
title: '确认登出?', </Flex>
cancelText: '保持登录', )}
okText: '确认登出', <Flex flex={1}>
okButtonProps: { <Button
icon: <LogoutOutlined />, block
danger: true, danger
}, icon={<LoginOutlined />}
onOk: () => signOut(`${window.location.origin}/user-center`), onClick={() => app.modal.confirm({
})} maskClosable: true,
> title: '确认登出?',
cancelText: '保持登录',
</Button> okText: '确认登出',
okButtonProps: {
icon: <LogoutOutlined />,
danger: true,
},
onOk: () => signOut(USER_CENTER_URL),
})}
>
</Button>
</Flex>
</Flex>
</Flex> </Flex>
</> </>
); );

View File

@ -1,8 +1,8 @@
import type { GamesData, XCXFindUserResp, XCXMember, XCXProfile, XCXTag } from "../types"; 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"; 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) { export function createXCXHeader(token: string) {
const xcxDefaultHeaders = { const xcxDefaultHeaders = {

View File

@ -1,6 +1,7 @@
import { chunk } from "lodash"; 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) { export function sneckGroup(size: number, groupLen: number) {
const indexArray = new Array<number>(size).fill(0).map((_, i) => i); 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) { if (url) {
window.open(url, '_blank'); 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 type { Player } from "../types";
import * as cheerio from "cheerio"; import * as cheerio from "cheerio";
import { XCXAPI } from "../services/xcxApi"; import { XCXAPI } from "../services/xcxApi";
import { BASE_URL } from "./common"; import { KAIQIU_BASE_URL, LOGTO_DOMAIN } from "./common";
import { RedisClient } from "bun"; import { RedisClient } from "bun";
import { createRemoteJWKSet, jwtVerify } from 'jose'; import { createRemoteJWKSet, jwtVerify } from 'jose';
import { LOGTO_RESOURCE } from "./constants"; import { LOGTO_RESOURCE } from "./constants";
@ -48,7 +48,7 @@ export const htmlRequestHeaders = {
* @returns HTML * @returns HTML
*/ */
export async function fetchEventContentHTML(matchId: string) { 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 }); const resp = await fetch(url, { headers: htmlRequestHeaders });
return resp.text() ?? '' return resp.text() ?? ''
} }
@ -97,7 +97,7 @@ export const extractBearerTokenFromHeaders = (authorization: string | null) => {
return authorization.slice(7); // The length of 'Bearer ' is 7 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) => { export const verifyLogtoToken = async (headers: Headers) => {
const auth = headers.get('Authorization'); const auth = headers.get('Authorization');
@ -110,7 +110,7 @@ export const verifyLogtoToken = async (headers: Headers) => {
jwks, jwks,
{ {
// Expected issuer of the token, issued by the Logto server // 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 // Expected audience token, the resource indicator of the current API
audience: LOGTO_RESOURCE, audience: LOGTO_RESOURCE,
} }