feat: migrate user fav system from session aud to Logto sub
- Update database schema to rename `UserFav` to `LogtoUserFav` with clearer field names (`logto_uid`, `kaiqiu_uid`). - Bump `jose` dependency to v6.2.1 for improved JWT verification. - Configure `@logto/react` to request the correct resource token for API access. - Implement token verification on the server side using `jose` and `jwtVerify`. - Update API routes (`/api/fav`) to extract the user ID from the verified JWT `sub` claim instead of the URL `aud` parameter. - Refactor frontend components (`FavButton`, `FavePlayersPage`) to use `useAuthHeaders` for fetching auth headers instead of manual token claims extraction. - Clean up unused migration and DAO functions related to the old `aud`-based logic.
This commit is contained in:
parent
ba8dfdf973
commit
9c9b3735cb
5
bun.lock
5
bun.lock
@ -14,6 +14,7 @@
|
|||||||
"cheerio": "^1.2.0",
|
"cheerio": "^1.2.0",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"ics": "^3.8.1",
|
"ics": "^3.8.1",
|
||||||
|
"jose": "^6.2.1",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
"mariadb": "^3.5.2",
|
"mariadb": "^3.5.2",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
@ -330,7 +331,7 @@
|
|||||||
|
|
||||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
"jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
|
"jose": ["jose@6.2.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="],
|
||||||
|
|
||||||
"js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="],
|
"js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="],
|
||||||
|
|
||||||
@ -496,6 +497,8 @@
|
|||||||
|
|
||||||
"@chevrotain/gast/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
"@chevrotain/gast/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||||
|
|
||||||
|
"@logto/client/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
|
||||||
|
|
||||||
"@prisma/adapter-mariadb/mariadb": ["mariadb@3.4.5", "", { "dependencies": { "@types/geojson": "^7946.0.16", "@types/node": "^24.0.13", "denque": "^2.1.0", "iconv-lite": "^0.6.3", "lru-cache": "^10.4.3" } }, "sha512-gThTYkhIS5rRqkVr+Y0cIdzr+GRqJ9sA2Q34e0yzmyhMCwyApf3OKAC1jnF23aSlIOqJuyaUFUcj7O1qZslmmQ=="],
|
"@prisma/adapter-mariadb/mariadb": ["mariadb@3.4.5", "", { "dependencies": { "@types/geojson": "^7946.0.16", "@types/node": "^24.0.13", "denque": "^2.1.0", "iconv-lite": "^0.6.3", "lru-cache": "^10.4.3" } }, "sha512-gThTYkhIS5rRqkVr+Y0cIdzr+GRqJ9sA2Q34e0yzmyhMCwyApf3OKAC1jnF23aSlIOqJuyaUFUcj7O1qZslmmQ=="],
|
||||||
|
|
||||||
"@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw=="],
|
"@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw=="],
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
"cheerio": "^1.2.0",
|
"cheerio": "^1.2.0",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"ics": "^3.8.1",
|
"ics": "^3.8.1",
|
||||||
|
"jose": "^6.2.1",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
"mariadb": "^3.5.2",
|
"mariadb": "^3.5.2",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE `UserFav` (
|
|
||||||
`aud` VARCHAR(191) NOT NULL,
|
|
||||||
`uid` VARCHAR(191) NOT NULL,
|
|
||||||
|
|
||||||
PRIMARY KEY (`aud`, `uid`)
|
|
||||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `LogtoUserFav` (
|
||||||
|
`logto_uid` VARCHAR(191) NOT NULL,
|
||||||
|
`kaiqiu_uid` VARCHAR(191) NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (`logto_uid`, `kaiqiu_uid`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
@ -13,8 +13,8 @@ datasource db {
|
|||||||
provider = "mysql"
|
provider = "mysql"
|
||||||
}
|
}
|
||||||
|
|
||||||
model UserFav {
|
model LogtoUserFav {
|
||||||
aud String
|
logto_uid String
|
||||||
uid String
|
kaiqiu_uid String
|
||||||
@@id([aud, uid])
|
@@id([logto_uid, kaiqiu_uid])
|
||||||
}
|
}
|
||||||
@ -4,6 +4,7 @@ import { useFavPlayerStore, type FavPlayer } from "../store/useFavPlayerStore";
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { useRequest } from "ahooks";
|
import { useRequest } from "ahooks";
|
||||||
import { useLogto } from "@logto/react";
|
import { useLogto } from "@logto/react";
|
||||||
|
import { useAuthHeaders } from "../hooks/useAuthHeaders";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user?: FavPlayer;
|
user?: FavPlayer;
|
||||||
@ -21,30 +22,30 @@ const StyledContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function FavButton(props: Props) {
|
export function FavButton(props: Props) {
|
||||||
const { isAuthenticated, getIdTokenClaims } = useLogto();
|
const { isAuthenticated } = useLogto();
|
||||||
const { fav, unFav, isFav } = useFavPlayerStore(store => store);
|
const { fav, unFav, isFav } = useFavPlayerStore(store => store);
|
||||||
|
const headers = useAuthHeaders();
|
||||||
const favReq = useRequest(async () => {
|
const favReq = useRequest(async () => {
|
||||||
if (!isAuthenticated) return;
|
if (!isAuthenticated) return;
|
||||||
const claims = await getIdTokenClaims();
|
await fetch(`/api/fav/${props.user?.uid}`, { method: 'PUT', headers });
|
||||||
await fetch(`/api/fav/${claims?.aud}/${props.user?.uid}`, { method: 'PUT' });
|
}, { manual: true });
|
||||||
}, { manual: true, refreshDeps: [props, isAuthenticated] });
|
|
||||||
const unFavReq = useRequest(async () => {
|
const unFavReq = useRequest(async () => {
|
||||||
if (!isAuthenticated) return;
|
if (!isAuthenticated) return;
|
||||||
const claims = await getIdTokenClaims();
|
await fetch(`/api/fav/${props.user?.uid}`, { method: 'DELETE', headers });
|
||||||
await fetch(`/api/fav/${claims?.aud}/${props.user?.uid}`, { method: 'DELETE' });
|
}, { manual: true });
|
||||||
}, { manual: true, refreshDeps: [props, isAuthenticated] });
|
|
||||||
const [value, setValue] = useState(isFav(props.user?.uid) ? 1 : 0);
|
const [value, setValue] = useState(isFav(props.user?.uid) ? 1 : 0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.user) return;
|
if (!props.user) return;
|
||||||
if (!isAuthenticated) return;
|
if (!isAuthenticated) return;
|
||||||
const id = setTimeout(async () => {
|
const id = setTimeout(async () => {
|
||||||
const claims = await getIdTokenClaims();
|
console.debug('Token', headers);
|
||||||
const aud = claims?.aud;
|
const res: { isFav: boolean } = await fetch(`/api/fav/${props.user?.uid}`, {
|
||||||
const res: { isFav: boolean } = await (await fetch(`/api/fav/${aud}/${props.user?.uid}`)).json();
|
headers,
|
||||||
|
}).then(res => res.json());
|
||||||
setValue(res.isFav ? 1 : 0);
|
setValue(res.isFav ? 1 : 0);
|
||||||
}, 300);
|
}, 300);
|
||||||
return () => clearTimeout(id);
|
return () => clearTimeout(id);
|
||||||
}, []);
|
}, [isAuthenticated, headers]);
|
||||||
const handleFavClick = useCallback((value: number) => {
|
const handleFavClick = useCallback((value: number) => {
|
||||||
if (!props.user) return;
|
if (!props.user) return;
|
||||||
setValue(value);
|
setValue(value);
|
||||||
|
|||||||
@ -1,30 +1,30 @@
|
|||||||
import { prisma } from "../prisma/db";
|
import { prisma } from "../prisma/db";
|
||||||
|
|
||||||
export async function updateUserFav(aud: string, uid: string) {
|
export async function updateUserFav(logto_uid: string, kaiqiu_uid: string) {
|
||||||
const size = await prisma.userFav.count({ where: { aud, uid }});
|
const size = await prisma.logtoUserFav.count({ where: { logto_uid, kaiqiu_uid }});
|
||||||
const isUnFav = size === 0;
|
const isUnFav = size === 0;
|
||||||
if (isUnFav) {
|
if (isUnFav) {
|
||||||
await prisma.userFav.create({
|
await prisma.logtoUserFav.create({
|
||||||
data: {
|
data: {
|
||||||
aud,
|
logto_uid,
|
||||||
uid,
|
kaiqiu_uid,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserFavList(aud: string) {
|
export async function getUserFavList(logto_uid: string) {
|
||||||
const result = await prisma.userFav.findMany({
|
const result = await prisma.logtoUserFav.findMany({
|
||||||
where: { aud },
|
where: { logto_uid },
|
||||||
select: { uid: true },
|
select: { kaiqiu_uid: true },
|
||||||
});
|
});
|
||||||
return result.map(e => e.uid);
|
return result.map(e => e.kaiqiu_uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteUserFav(aud: string, uid: string) {
|
export async function deleteUserFav(logto_uid: string, kaiqiu_uid: string) {
|
||||||
await prisma.userFav.deleteMany({ where: { aud, uid }});
|
await prisma.logtoUserFav.deleteMany({ where: { logto_uid, kaiqiu_uid }});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function isUserFav(aud: string, uid: string) {
|
export async function isUserFav(logto_uid: string, kaiqiu_uid: string) {
|
||||||
return (await prisma.userFav.count({ where: { aud, uid }})) > 0;
|
return (await prisma.logtoUserFav.count({ where: { kaiqiu_uid, logto_uid }})) > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,12 +12,14 @@ 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";
|
||||||
|
|
||||||
const elem = document.getElementById("root")!;
|
const elem = document.getElementById("root")!;
|
||||||
|
|
||||||
const config: LogtoConfig = {
|
const config: LogtoConfig = {
|
||||||
endpoint: 'https://logto.ksr.la/',
|
endpoint: 'https://logto.ksr.la/',
|
||||||
appId: 'iq0oceaeqazpcned7hzil',
|
appId: 'iq0oceaeqazpcned7hzil',
|
||||||
|
resources: [LOGTO_RESOURCE],
|
||||||
};
|
};
|
||||||
const app = (
|
const app = (
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|||||||
16
src/hooks/useAuthHeaders.ts
Normal file
16
src/hooks/useAuthHeaders.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { useLogto } from "@logto/react"
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { LOGTO_RESOURCE } from "../utils/constants";
|
||||||
|
|
||||||
|
export const useAuthHeaders = (): HeadersInit => {
|
||||||
|
const { isAuthenticated, getAccessToken } = useLogto();
|
||||||
|
const [headers, setHeaders] = useState<HeadersInit>({});
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
getAccessToken(LOGTO_RESOURCE).then(token => {
|
||||||
|
setHeaders({ Authorization: `Bearer ${token}` })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isAuthenticated]);
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { serve } from "bun";
|
import { serve } from "bun";
|
||||||
import { getMatchInfo, xcxApi } from "./utils/server";
|
import { getMatchInfo, verifyLogtoToken, xcxApi } from "./utils/server";
|
||||||
import ics from 'ics';
|
import ics from 'ics';
|
||||||
import index from "./index.html";
|
import index from "./index.html";
|
||||||
import { getUidScore } from "./services/uidScoreStore";
|
import { getUidScore } from "./services/uidScoreStore";
|
||||||
@ -151,23 +151,27 @@ const server = serve({
|
|||||||
return Response.json(profile);
|
return Response.json(profile);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"/api/fav/:aud": {
|
"/api/fav": {
|
||||||
async GET(req) {
|
async GET(req) {
|
||||||
const list = await listFavPlayers(req.params.aud);
|
const payload = await verifyLogtoToken(req.headers);
|
||||||
|
const list = await listFavPlayers(`${payload.sub}`);
|
||||||
return Response.json(list.filter(Boolean));
|
return Response.json(list.filter(Boolean));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/fav/:aud/:uid": {
|
"/api/fav/:uid": {
|
||||||
async GET(req) {
|
async GET(req) {
|
||||||
const isFav = await checkIsUserFav(req.params.aud, req.params.uid);
|
const payload = await verifyLogtoToken(req.headers);
|
||||||
|
const isFav = await checkIsUserFav(`${payload.sub}`, req.params.uid);
|
||||||
return Response.json({ isFav });
|
return Response.json({ isFav });
|
||||||
},
|
},
|
||||||
async PUT(req) {
|
async PUT(req) {
|
||||||
await favPlayer(req.params.aud, req.params.uid);
|
const payload = await verifyLogtoToken(req.headers);
|
||||||
|
await favPlayer(`${payload.sub}`, req.params.uid);
|
||||||
return Response.json({ ok: 'ok' });
|
return Response.json({ ok: 'ok' });
|
||||||
},
|
},
|
||||||
async DELETE(req) {
|
async DELETE(req) {
|
||||||
await unFavPlayer(req.params.aud, req.params.uid);
|
const payload = await verifyLogtoToken(req.headers);
|
||||||
|
await unFavPlayer(`${payload.sub}`, req.params.uid);
|
||||||
return Response.json({ ok: 'ok' });
|
return Response.json({ ok: 'ok' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { DeleteOutlined, LoginOutlined, SyncOutlined, UploadOutlined } from "@an
|
|||||||
import { useRequest } from "ahooks";
|
import { useRequest } from "ahooks";
|
||||||
import type { XCXProfile } from "../types";
|
import type { XCXProfile } from "../types";
|
||||||
import useAutoLogin from "../hooks/useAutoLogin";
|
import useAutoLogin from "../hooks/useAutoLogin";
|
||||||
|
import { useAuthHeaders } from "../hooks/useAuthHeaders";
|
||||||
|
|
||||||
enum SortType {
|
enum SortType {
|
||||||
DEFAULT = '注册时间',
|
DEFAULT = '注册时间',
|
||||||
@ -24,10 +25,11 @@ export function FavePlayersPage() {
|
|||||||
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 { login } = useAutoLogin();
|
const { login } = 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/${aud}`);
|
const res = await fetch(`/api/fav`, { headers });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
return data;
|
return data;
|
||||||
}, { manual: true, cacheKey: 'favListRequest', cacheTime: 3 * 60 * 1000 });
|
}, { manual: true, cacheKey: 'favListRequest', cacheTime: 3 * 60 * 1000 });
|
||||||
@ -61,7 +63,7 @@ export function FavePlayersPage() {
|
|||||||
const claims = await getIdTokenClaims()!;
|
const claims = await getIdTokenClaims()!;
|
||||||
const aud = claims?.aud;
|
const aud = claims?.aud;
|
||||||
const jobs = list.map(async u => {
|
const jobs = list.map(async u => {
|
||||||
await fetch(`/api/fav/${aud}/${u.uid}`, { method: 'PUT' });
|
await fetch(`/api/fav/${u.uid}`, { method: 'PUT', headers });
|
||||||
});
|
});
|
||||||
message.open({ key: 'sync', content: '同步中...', type: 'loading' });
|
message.open({ key: 'sync', content: '同步中...', type: 'loading' });
|
||||||
await Promise.all(jobs);
|
await Promise.all(jobs);
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
|
export const LOGTO_RESOURCE = 'https://tt.ksr.la';
|
||||||
export const CLUB_SELECTOR_KEY = 'CLUB_SELECTOR';
|
export const CLUB_SELECTOR_KEY = 'CLUB_SELECTOR';
|
||||||
export const STORE_PAGE_LIST_KEY = 'events-page-keys';
|
export const STORE_PAGE_LIST_KEY = 'events-page-keys';
|
||||||
@ -1,9 +1,10 @@
|
|||||||
import type { IEventInfo, 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 { BASE_URL } from "./common";
|
||||||
import { RedisClient } from "bun";
|
import { RedisClient } from "bun";
|
||||||
import dayjs from "dayjs";
|
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
||||||
|
import { LOGTO_RESOURCE } from "./constants";
|
||||||
|
|
||||||
const REQUIRED_ENVS = [
|
const REQUIRED_ENVS = [
|
||||||
process.env.KAIQIUCC_TOKEN,
|
process.env.KAIQIUCC_TOKEN,
|
||||||
@ -40,62 +41,6 @@ export const htmlRequestHeaders = {
|
|||||||
"cookie": "SECKEY_ABVK=oTGgqH4ypGPFVdQ3J9K7PoAOPdZ+8R7CsUzi75gelcg%3D; uchome_sendmail=1"
|
"cookie": "SECKEY_ABVK=oTGgqH4ypGPFVdQ3J9K7PoAOPdZ+8R7CsUzi75gelcg%3D; uchome_sendmail=1"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param tagid 俱乐部 ID
|
|
||||||
*/
|
|
||||||
export async function listEvent(tagid: string): Promise<IEventInfo[]> {
|
|
||||||
const key = `my-kaiqiuwang:club-events:${tagid}`;
|
|
||||||
let html = await redis.get(key).catch(() => '');
|
|
||||||
if (!html) {
|
|
||||||
html = await fetchEventListHTML(tagid);
|
|
||||||
redis.setex(key, 60 * 60 * REDIS_CACHE_HOUR, html);
|
|
||||||
}
|
|
||||||
return await parseEventList(html);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param tagid 俱乐部 ID
|
|
||||||
* @return HTML
|
|
||||||
*/
|
|
||||||
export async function fetchEventListHTML(tagid: string) {
|
|
||||||
const url = `${BASE_URL}/home/space.php?do=mtag&tagid=${tagid}&view=event`;
|
|
||||||
const resp = await fetch(url, { headers: htmlRequestHeaders });
|
|
||||||
return resp.text() ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function parseEventList(html: string) {
|
|
||||||
const $ = cheerio.load(html);
|
|
||||||
const blockList = $('div.event_list > ol > li');
|
|
||||||
const list: IEventInfo[] = [];
|
|
||||||
for (const block of blockList) {
|
|
||||||
const titleEl = $(block).find('.event_title');
|
|
||||||
const title = titleEl.text();
|
|
||||||
const eventPath = $(titleEl).find('a').attr('href') ?? '';
|
|
||||||
const place = $(block).find('ul li:nth-of-type(2)').text().replace('比赛地点: \\t', '').trim();
|
|
||||||
const eventURL = `${BASE_URL}/home/${eventPath}`;
|
|
||||||
const matchId = /\S+-(\d+).html$/.exec(eventPath)?.[1] ?? '';
|
|
||||||
let eventPage = await redis.get(`my-kaiqiuwang:match:${matchId}`) ?? '';
|
|
||||||
if (!eventPage) {
|
|
||||||
eventPage = await fetch(eventURL, { headers: htmlRequestHeaders }).then(res => res.text() ?? '');
|
|
||||||
await redis.setex(`my-kaiqiuwang:match:${matchId}`, 60 * 60 * 10, eventPage)
|
|
||||||
}
|
|
||||||
const $eventPage = cheerio.load(eventPage);
|
|
||||||
const eventContent = $eventPage('.event_content').text().replace(/(\r|\n)/g, ',').split(',').filter(Boolean).join(' ');
|
|
||||||
const { y, M, D, H, m} = /比赛开始:.*?(?<y>\d{4})年(?<M>\d{2})月(?<D>\d{2})日 \w+ (?<H>\d{2}):(?<m>\d{2})/
|
|
||||||
.exec(eventContent)?.groups ?? {};
|
|
||||||
const startDate = y ? `${y}-${M}-${D} ${H}:${m}` : '';
|
|
||||||
const event: IEventInfo = {
|
|
||||||
title,
|
|
||||||
info: [`比赛时间:${startDate}`, place],
|
|
||||||
url: eventURL,
|
|
||||||
startDate,
|
|
||||||
matchId,
|
|
||||||
isFinished: dayjs(startDate).isBefore(),
|
|
||||||
}
|
|
||||||
list.push(event);
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -139,3 +84,38 @@ export async function getMatchInfo(matchId: string) {
|
|||||||
}
|
}
|
||||||
return parseEventInfo(html);
|
return parseEventInfo(html);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const extractBearerTokenFromHeaders = (authorization: string | null) => {
|
||||||
|
if (!authorization) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authorization.startsWith('Bearer')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorization.slice(7); // The length of 'Bearer ' is 7
|
||||||
|
};
|
||||||
|
|
||||||
|
const jwks = createRemoteJWKSet(new URL('https://logto.ksr.la/oidc/jwks'));
|
||||||
|
|
||||||
|
export const verifyLogtoToken = async (headers: Headers) => {
|
||||||
|
const auth = headers.get('Authorization');
|
||||||
|
const token = extractBearerTokenFromHeaders(auth)
|
||||||
|
// console.debug('Authorization', { auth, token });
|
||||||
|
if (!token) return {};
|
||||||
|
const { payload } = await jwtVerify(
|
||||||
|
// The raw Bearer Token extracted from the request header
|
||||||
|
token,
|
||||||
|
jwks,
|
||||||
|
{
|
||||||
|
// Expected issuer of the token, issued by the Logto server
|
||||||
|
issuer: 'https://logto.ksr.la/oidc',
|
||||||
|
// Expected audience token, the resource indicator of the current API
|
||||||
|
audience: LOGTO_RESOURCE,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// console.debug('Payload', payload);
|
||||||
|
// Sub is the user ID, used for user identification
|
||||||
|
return payload
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user