diff --git a/package.json b/package.json index b106079aa..161c97824 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "hast-util-to-jsx-runtime": "2.3.0", "lethargy": "1.0.9", "lucide-react": "0.378.0", + "ofetch": "1.3.4", "react-hook-form": "7.51.4", "react-router-dom": "6.23.1", "rehype-infer-description-meta": "2.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5285e2be7..d7f9519d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: lucide-react: specifier: 0.378.0 version: 0.378.0(react@18.3.1) + ofetch: + specifier: 1.3.4 + version: 1.3.4 react-hook-form: specifier: 7.51.4 version: 7.51.4(react@18.3.1) @@ -2094,6 +2097,9 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + destr@2.0.3: + resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} + detect-libc@2.0.3: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} @@ -3344,6 +3350,9 @@ packages: node-api-version@0.2.0: resolution: {integrity: sha512-fthTTsi8CxaBXMaBAD7ST2uylwvsnYxh2PfaScwpMhos6KlSFajXQPcM4ogNE1q2s3Lbz9GCGqeIHC+C6OZnKg==} + node-fetch-native@1.6.4: + resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} + node-gyp@9.4.1: resolution: {integrity: sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==} engines: {node: ^12.13 || ^14.13 || >=16} @@ -3421,6 +3430,9 @@ packages: resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} engines: {node: '>= 0.4'} + ofetch@1.3.4: + resolution: {integrity: sha512-KLIET85ik3vhEfS+3fDlc/BAZiAp+43QEC/yCo5zkNoY2YaKvNkOaFr/6wCFgFH1kuYQM5pMNi0Tg8koiIemtw==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -6423,6 +6435,8 @@ snapshots: dequal@2.0.3: {} + destr@2.0.3: {} + detect-libc@2.0.3: {} detect-node-es@1.1.0: {} @@ -8111,6 +8125,8 @@ snapshots: dependencies: semver: 7.6.2 + node-fetch-native@1.6.4: {} + node-gyp@9.4.1: dependencies: env-paths: 2.2.1 @@ -8201,6 +8217,12 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 + ofetch@1.3.4: + dependencies: + destr: 2.0.3 + node-fetch-native: 1.6.4 + ufo: 1.5.3 + once@1.4.0: dependencies: wrappy: 1.0.2 diff --git a/src/renderer/src/components/feed-column/list.tsx b/src/renderer/src/components/feed-column/list.tsx index 413bb5df7..b4e2edcae 100644 --- a/src/renderer/src/components/feed-column/list.tsx +++ b/src/renderer/src/components/feed-column/list.tsx @@ -18,10 +18,10 @@ import { ContextMenuSeparator, } from "@renderer/components/ui/context-menu" import { useMutation, useQueryClient } from "@tanstack/react-query" -import { getCsrfToken } from "@hono/auth-js/react" import { useToast } from "@renderer/components/ui/use-toast" import { ToastAction } from "@renderer/components/ui/toast" import { SubscriptionResponse } from "@renderer/lib/types" +import { apiFetch } from "@renderer/lib/queries/api-fetch" export function FeedList({ className, @@ -93,23 +93,13 @@ function FeedCategory({ const queryClient = useQueryClient() const deleteMutation = useMutation({ - mutationFn: async (feed: SubscriptionResponse[number]) => { - return ( - await ( - await fetch(`${import.meta.env.VITE_API_URL}/subscriptions`, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - credentials: "include", - body: JSON.stringify({ - feedId: feed.feedId, - csrfToken: await getCsrfToken(), - }), - }) - ).json() - ).data - }, + mutationFn: async (feed: SubscriptionResponse[number]) => + apiFetch("/subscriptions", { + method: "DELETE", + body: { + feedId: feed.feedId, + }, + }), onSuccess: (_, variables) => { queryClient.invalidateQueries({ queryKey: ["subscriptions", view], @@ -126,19 +116,14 @@ function FeedCategory({ { - await fetch(`${import.meta.env.VITE_API_URL}/subscriptions`, { + await apiFetch("/subscriptions", { method: "POST", - headers: { - "Content-Type": "application/json", - }, - credentials: "include", - body: JSON.stringify({ + body: { url: variables.feeds.url, view, category: variables.category, // private: variables.private, - csrfToken: await getCsrfToken(), - }), + }, }) queryClient.invalidateQueries({ queryKey: ["subscriptions", view], diff --git a/src/renderer/src/components/follow/dialog.tsx b/src/renderer/src/components/follow/dialog.tsx index 108339243..2a0f03d47 100644 --- a/src/renderer/src/components/follow/dialog.tsx +++ b/src/renderer/src/components/follow/dialog.tsx @@ -12,7 +12,6 @@ import { } from "@renderer/components/ui/form" import { Input } from "@renderer/components/ui/input" import { useMutation, useQueryClient } from "@tanstack/react-query" -import { getCsrfToken } from "@hono/auth-js/react" import { DialogContent, DialogHeader, @@ -31,6 +30,7 @@ import { views } from "@renderer/lib/constants" import { cn } from "@renderer/lib/utils" import { Switch } from "@renderer/components/ui/switch" import { FollowSummary } from "../feed-summary" +import { apiFetch } from "@renderer/lib/queries/api-fetch" const formSchema = z.object({ view: z.enum(["0", "1", "2", "3", "4", "5"]), @@ -54,26 +54,16 @@ export function FollowDialog({ const queryClient = useQueryClient() const followMutation = useMutation({ - mutationFn: async (values: z.infer) => { - return ( - await ( - await fetch(`${import.meta.env.VITE_API_URL}/subscriptions`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - credentials: "include", - body: JSON.stringify({ - url: feed.url, - view: parseInt(values.view), - category: values.category, - private: values.private, - csrfToken: await getCsrfToken(), - }), - }) - ).json() - ).data - }, + mutationFn: async (values: z.infer) => + apiFetch("/subscriptions", { + method: "POST", + body: { + url: feed.url, + view: parseInt(values.view), + category: values.category, + private: values.private, + }, + }), onSuccess: (_, variables) => { queryClient.invalidateQueries({ queryKey: ["subscriptions", parseInt(variables.view)], diff --git a/src/renderer/src/components/follow/form.tsx b/src/renderer/src/components/follow/form.tsx index 35772cfcf..fc7c2d8f1 100644 --- a/src/renderer/src/components/follow/form.tsx +++ b/src/renderer/src/components/follow/form.tsx @@ -13,7 +13,6 @@ import { import { Input } from "@renderer/components/ui/input" import { useEffect } from "react" import { useMutation } from "@tanstack/react-query" -import { getCsrfToken } from "@hono/auth-js/react" import { Image } from "@renderer/components/ui/image" import { FeedResponse, EntriesResponse } from "@renderer/lib/types" import { @@ -24,6 +23,7 @@ import { } from "@renderer/components/ui/card" import { FollowButton } from "./button" import { FollowSummary } from "../feed-summary" +import { apiFetch } from "@renderer/lib/queries/api-fetch" const formSchema = z.object({ keyword: z.string().min(1), @@ -61,28 +61,22 @@ export function FollowForm({ type }: { type: string }) { }) const mutation = useMutation({ mutationFn: async (keyword: string) => { - return ( - await ( - await fetch(`${import.meta.env.VITE_API_URL}/discover`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - credentials: "include", - body: JSON.stringify({ - csrfToken: await getCsrfToken(), - keyword, - type: type === "rss" ? "rss" : "auto", - }), - }) - ).json() - ).data as { - feed: Partial - docs?: string - entries?: Partial - isSubscribed?: boolean - subscriptionCount?: number - }[] + const { data } = await apiFetch<{ + data: { + feed: Partial + docs?: string + entries?: Partial + isSubscribed?: boolean + subscriptionCount?: number + }[] + }>("/discover", { + method: "POST", + body: { + keyword, + type: type === "rss" ? "rss" : "auto", + }, + }) + return data }, }) diff --git a/src/renderer/src/lib/queries/api-fetch.ts b/src/renderer/src/lib/queries/api-fetch.ts new file mode 100644 index 000000000..2433e93b6 --- /dev/null +++ b/src/renderer/src/lib/queries/api-fetch.ts @@ -0,0 +1,19 @@ +import { ofetch } from "ofetch" +import { getCsrfToken } from "@hono/auth-js/react" + +export const apiFetch = ofetch.create({ + baseURL: import.meta.env.VITE_API_URL, + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + onRequest: async ({ options }) => { + if (options.method && options.method.toLowerCase() !== "get") { + const csrfToken = await getCsrfToken() + if (!options.body) { + options.body = {} + } + ;(options.body as Record).csrfToken = csrfToken + } + }, +}) diff --git a/src/renderer/src/lib/queries/entries.ts b/src/renderer/src/lib/queries/entries.ts index ceea422f6..2792b2823 100644 --- a/src/renderer/src/lib/queries/entries.ts +++ b/src/renderer/src/lib/queries/entries.ts @@ -1,6 +1,7 @@ import { useInfiniteQuery } from "@tanstack/react-query" import { levels } from "@renderer/lib/constants" import { EntriesResponse, ListResponse } from "../types" +import { apiFetch } from "@renderer/lib/queries/api-fetch" export const useEntries = ({ level, @@ -13,8 +14,6 @@ export const useEntries = ({ queryKey: ["entries", level, id], enabled: level !== undefined && id !== undefined, queryFn: async ({ pageParam }) => { - let entries: ListResponse | null = null - const baseUrl = `${import.meta.env.VITE_API_URL}/entries?` const params: { category?: string view?: string @@ -27,26 +26,12 @@ export const useEntries = ({ } else if (level === levels.feed) { params.feedId = id + "" } - entries = await ( - await fetch( - baseUrl + - new URLSearchParams({ - offset: pageParam + "", - ...params, - }), - { - credentials: "include", - }, - ) - ).json() - - if (!entries) { - entries = { - code: -1, - } - } - - return entries + return await apiFetch>("/entries", { + query: { + offset: pageParam + "", + ...params, + }, + }) }, getNextPageParam: (lastPage, _, lastPageParam) => lastPageParam + (lastPage.data?.length || 0), diff --git a/src/renderer/src/lib/queries/subscriptions.ts b/src/renderer/src/lib/queries/subscriptions.ts index c110dcdee..9ce269bde 100644 --- a/src/renderer/src/lib/queries/subscriptions.ts +++ b/src/renderer/src/lib/queries/subscriptions.ts @@ -1,5 +1,6 @@ import { useQuery } from "@tanstack/react-query" import { SubscriptionResponse } from "../types" +import { apiFetch } from "@renderer/lib/queries/api-fetch" export type Response = { list: { @@ -14,17 +15,13 @@ export const useSubscriptions = (view?: number) => useQuery({ queryKey: ["subscriptions", view], queryFn: async () => { - const subscriptions = ( - await ( - await fetch( - `${import.meta.env.VITE_API_URL}/subscriptions` + - (view !== undefined ? `?view=${view}` : ``), - { - credentials: "include", - }, - ) - ).json() - ).data as SubscriptionResponse + const { data: subscriptions } = await apiFetch<{ + data: SubscriptionResponse + }>("/subscriptions", { + query: { + view, + }, + }) const categories = { list: {}, diff --git a/src/renderer/src/pages/redirect.tsx b/src/renderer/src/pages/redirect.tsx index 3b64b11b2..f32b1639f 100644 --- a/src/renderer/src/pages/redirect.tsx +++ b/src/renderer/src/pages/redirect.tsx @@ -1,29 +1,21 @@ import { Button } from "@renderer/components/ui/button" import { UserButton } from "@renderer/components/user-button" -import { getCsrfToken, authConfigManager } from "@hono/auth-js/react" import { useEffect } from "react" import { useNavigate } from "react-router-dom" +import { apiFetch } from "@renderer/lib/queries/api-fetch" export function Component() { const navigate = useNavigate() const getCallbackUrl = async () => { - const response = await ( - await fetch( - `${authConfigManager.getConfig().baseUrl}/auth-app/new-session`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - credentials: "include", - body: JSON.stringify({ - csrfToken: await getCsrfToken(), - }), - }, - ) - ).json() - return `follow://auth?token=${response.data.sessionToken}` + const { data } = await apiFetch<{ + data: { + sessionToken: string + } + }>("/auth-app/new-session", { + method: "POST", + }) + return `follow://auth?token=${data.sessionToken}` } useEffect(() => {