diff --git a/server/api/oauth/github.ts b/server/api/oauth/github.ts index 46fd9b0..9395686 100644 --- a/server/api/oauth/github.ts +++ b/server/api/oauth/github.ts @@ -63,8 +63,8 @@ export default defineEventHandler(async (event) => { const params = new URLSearchParams({ login: "github", - user_jwt: jwtToken, - user_info: JSON.stringify({ + jwt: jwtToken, + user: JSON.stringify({ avatar: userInfo.avatar_url, name: userInfo.name, }), diff --git a/server/middleware/auth.ts b/server/middleware/auth.ts index 9faad5e..6374b03 100644 --- a/server/middleware/auth.ts +++ b/server/middleware/auth.ts @@ -6,13 +6,13 @@ export default defineEventHandler(async (event) => { console.log(url.pathname) if (["JWT_SECRET", "G_CLIENT_ID", "G_CLIENT_SECRET"].find(k => !process.env[k])) { event.context.disabledLogin = true - if (url.pathname.startsWith("/me")) throw createError({ statusCode: 506, message: "Server not configured" }) + if (url.pathname.startsWith("/api/me")) throw createError({ statusCode: 506, message: "Server not configured" }) } else { if (/^\/api\/(?:me|s)\//.test(url.pathname)) { - const token = getHeader(event, "Authorization") + const token = getHeader(event, "Authorization")?.replace("Bearer ", "")?.trim() if (token && process.env.JWT_SECRET) { try { - const { payload } = await jwtVerify(token.replace("Bearer ", ""), new TextEncoder().encode(process.env.JWT_SECRET)) as { payload?: { id: string, type: string } } + const { payload } = await jwtVerify(token, new TextEncoder().encode(process.env.JWT_SECRET)) as { payload?: { id: string, type: string } } if (payload?.id) { event.context.user = { id: payload.id, @@ -20,7 +20,7 @@ export default defineEventHandler(async (event) => { } } } catch { - if (url.pathname.startsWith("/me")) throw createError({ statusCode: 401, message: "JWT verification failed" }) + if (url.pathname.startsWith("/api/me")) throw createError({ statusCode: 401, message: "JWT verification failed" }) logger.warn("JWT verification failed") } } diff --git a/src/components/column/card.tsx b/src/components/column/card.tsx index d0713e0..5b65242 100644 --- a/src/components/column/card.tsx +++ b/src/components/column/card.tsx @@ -11,6 +11,7 @@ import { ofetch } from "ofetch" import { OverlayScrollbar } from "../common/overlay-scrollbar" import { focusSourcesAtom, refetchSourcesAtom } from "~/atoms" import { useRelativeTime } from "~/hooks/useRelativeTime" +import { safeParseString } from "~/utils" export interface ItemsProps extends React.HTMLAttributes { id: SourceID @@ -71,7 +72,7 @@ function NewsCard({ id, inView, handleListeners }: NewsCardProps) { const response: SourceResponse = await ofetch(url, { timeout: 10000, headers: { - Authorization: `Bearer ${localStorage.getItem("user_jwt")}`, + Authorization: `Bearer ${safeParseString(localStorage.getItem("jwt"))}`, }, }) return response diff --git a/src/hooks/useLogin.ts b/src/hooks/useLogin.ts new file mode 100644 index 0000000..ae3941f --- /dev/null +++ b/src/hooks/useLogin.ts @@ -0,0 +1,27 @@ +import { useAtom } from "jotai" +import { atomWithStorage } from "jotai/utils" +import { useCallback } from "react" + +const userAtom = atomWithStorage<{ + name?: string + avatar?: string +}>("user", {}) + +const jwtAtom = atomWithStorage("jwt", "") + +export function useLogin() { + const [userInfo] = useAtom(userAtom) + const [jwt, setJwt] = useAtom(jwtAtom) + const enabledLogin = __ENABLE_LOGIN__ + const login = useCallback(() => { + window.location.href = `https://github.com/login/oauth/authorize?client_id=${__G_CLIENT_ID__}` + }, []) + + return { + enabledLogin, + loggedIn: !!jwt, + userInfo, + logout: () => setJwt(""), + login, + } +} diff --git a/src/hooks/useSync.ts b/src/hooks/useSync.ts index d64d5f9..474426c 100644 --- a/src/hooks/useSync.ts +++ b/src/hooks/useSync.ts @@ -1,13 +1,15 @@ import type { PrimitiveMetadata } from "@shared/types" import { useAtom } from "jotai" import { ofetch } from "ofetch" -import { useEffect } from "react" -import { useDebounce } from "react-use" +import { useDebounce, useMount } from "react-use" +import { toast } from "sonner" +import { useLogin } from "./useLogin" import { preprocessMetadata, primitiveMetadataAtom } from "~/atoms" +import { safeParseString } from "~/utils" export async function uploadMetadata(metadata: PrimitiveMetadata) { if (!__ENABLE_LOGIN__) return - const jwt = localStorage.getItem("user_jwt") + const jwt = localStorage.getItem("jwt") if (!jwt) return try { await ofetch("/api/me/sync", { @@ -27,42 +29,51 @@ export async function uploadMetadata(metadata: PrimitiveMetadata) { export async function downloadMetadata(): Promise { if (!__ENABLE_LOGIN__) return - const jwt = localStorage.getItem("user_jwt") + const jwt = safeParseString(localStorage.getItem("jwt")) if (!jwt) return - try { - const { data, updatedTime } = await ofetch("/api/me/sync", { - headers: { - Authorization: `Bearer ${jwt}`, - }, - }) as PrimitiveMetadata - // 不用同步 action 字段 - if (data) { - return { - action: "sync", - data, - updatedTime, - } + const { data, updatedTime } = await ofetch("/api/me/sync", { + headers: { + Authorization: `Bearer ${jwt}`, + }, + }) as PrimitiveMetadata + // 不用同步 action 字段 + if (data) { + return { + action: "sync", + data, + updatedTime, } - } catch (e) { - console.error(e) } } export function useSync() { const [primitiveMetadata, setPrimitiveMetadata] = useAtom(primitiveMetadataAtom) + const { logout, login } = useLogin() useDebounce(async () => { if (primitiveMetadata.action === "manual") { uploadMetadata(primitiveMetadata) } }, 10000, [primitiveMetadata]) - useEffect(() => { + useMount(() => { const fn = async () => { - const metadata = await downloadMetadata() - if (metadata) { - setPrimitiveMetadata(preprocessMetadata(metadata)) + try { + const metadata = await downloadMetadata() + if (metadata) { + setPrimitiveMetadata(preprocessMetadata(metadata)) + } + } catch (e: any) { + if (e.statusCode === 401) { + toast.error("身份校验失败,请重新登录", { + action: { + label: "登录", + onClick: login, + }, + }) + logout() + } } } fn() - }, [setPrimitiveMetadata]) + }) } diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 116e437..d2a121e 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -10,6 +10,7 @@ import { useOnReload } from "~/hooks/useOnReload" import { GlobalOverlayScrollbar } from "~/components/common/overlay-scrollbar" import { useSync } from "~/hooks/useSync" import { Footer } from "~/components/footer" +import { Toast } from "~/components/common/toast" export const Route = createRootRouteWithContext<{ queryClient: QueryClient @@ -18,8 +19,9 @@ export const Route = createRootRouteWithContext<{ notFoundComponent: NotFoundComponent, beforeLoad: () => { const query = new URLSearchParams(window.location.search) - if (query.has("login")) { - [...query.entries()].forEach(key => localStorage.setItem(key[0], key[1])) + if (query.has("login") && query.has("user") && query.has("jwt")) { + localStorage.setItem("user", query.get("user")!) + localStorage.setItem("jwt", JSON.stringify(query.get("jwt")!)) window.history.replaceState({}, document.title, window.location.pathname) } }, @@ -46,15 +48,15 @@ function RootComponent() {
@@ -63,6 +65,7 @@ function RootComponent() {
+ {import.meta.env.DEV && ( <> diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..1554b52 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,7 @@ +export function safeParseString(str: any) { + try { + return JSON.parse(str) + } catch { + return "" + } +}