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:
parent
de05ca2ecf
commit
f188b4eac4
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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}` })
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
) }
|
) }
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -91,3 +91,7 @@ export const openWebMapRaw = (type: MapType, location: MapLocation): void => {
|
|||||||
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`;
|
||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user