diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7cfdd4cd..6ca8731d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -96,9 +96,6 @@ jobs: - name: copy .env.local to apps/readest-app run: cp .env.local apps/readest-app/.env.local - - name: fix dynamic route for Next.js, see https://github.com/vercel/next.js/discussions/55393 - run: rimraf "apps/readest-app/src/app/reader/[ids]" - - name: install dependencies (ubuntu only) if: matrix.config.os == 'ubuntu-latest' run: | diff --git a/apps/readest-app/package.json b/apps/readest-app/package.json index 50d6d45d..8ae78f6a 100644 --- a/apps/readest-app/package.json +++ b/apps/readest-app/package.json @@ -44,6 +44,7 @@ "@tauri-apps/plugin-updater": "^2.0.0", "@zip.js/zip.js": "^2.7.53", "clsx": "^2.1.1", + "cors": "^2.8.5", "cssbeautify": "^0.3.1", "foliate-js": "workspace:*", "js-md5": "^0.8.3", @@ -57,6 +58,7 @@ }, "devDependencies": { "@tauri-apps/cli": "2.1.0", + "@types/cors": "^2.8.17", "@types/cssbeautify": "^0.3.5", "@types/node": "^22.10.1", "@types/react": "18.3.12", diff --git a/apps/readest-app/src-tauri/tauri.conf.json b/apps/readest-app/src-tauri/tauri.conf.json index 77f350bc..0f980ef0 100644 --- a/apps/readest-app/src-tauri/tauri.conf.json +++ b/apps/readest-app/src-tauri/tauri.conf.json @@ -14,7 +14,7 @@ "security": { "csp": { "default-src": "'self' 'unsafe-inline' blob: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost https://fonts.gstatic.com https://db.onlinewebfonts.com", - "connect-src": "'self' blob: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co", + "connect-src": "'self' blob: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com", "img-src": "'self' blob: data: asset: http://asset.localhost https://*", "style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost", "frame-src": "'self' blob: asset: http://asset.localhost", diff --git a/apps/readest-app/src/app/layout.tsx b/apps/readest-app/src/app/layout.tsx index d9cf7139..2970069e 100644 --- a/apps/readest-app/src/app/layout.tsx +++ b/apps/readest-app/src/app/layout.tsx @@ -1,8 +1,5 @@ import * as React from 'react'; -import { AuthProvider } from '@/context/AuthContext'; -import { EnvProvider } from '@/context/EnvContext'; -import { CSPostHogProvider } from '@/context/PHContext'; -import { SyncProvider } from '@/context/SyncContext'; +import Providers from '@/components/Providers'; import '../styles/globals.css'; import '../styles/fonts.css'; @@ -39,15 +36,9 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - - - - - {children} - - - - + + {children} + ); } diff --git a/apps/readest-app/src/app/library/components/Bookshelf.tsx b/apps/readest-app/src/app/library/components/Bookshelf.tsx index 6a6f04be..9e8e564f 100644 --- a/apps/readest-app/src/app/library/components/Bookshelf.tsx +++ b/apps/readest-app/src/app/library/components/Bookshelf.tsx @@ -56,7 +56,7 @@ const Bookshelf: React.FC = ({ libraryBooks, isSelectMode, onImp const [selectedBooks, setSelectedBooks] = useState([]); const [showDeleteAlert, setShowDeleteAlert] = useState(false); const [clickedImage, setClickedImage] = useState(null); - const [importBookUrl] = useState(searchParams.get('url') || ''); + const [importBookUrl] = useState(searchParams?.get('url') || ''); const isImportingBook = useRef(false); const { setLibrary } = useLibraryStore(); diff --git a/apps/readest-app/src/app/reader/[ids]/page.tsx b/apps/readest-app/src/app/reader/[ids]/page.tsx deleted file mode 100644 index 94c4a8d9..00000000 --- a/apps/readest-app/src/app/reader/[ids]/page.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import Reader from '../components/Reader'; - -export default async function Page({ params }: { params: Promise<{ ids: string }> }) { - const ids = decodeURIComponent((await params).ids); - return ; -} diff --git a/apps/readest-app/src/app/reader/components/ReaderContent.tsx b/apps/readest-app/src/app/reader/components/ReaderContent.tsx index 2307711d..527de231 100644 --- a/apps/readest-app/src/app/reader/components/ReaderContent.tsx +++ b/apps/readest-app/src/app/reader/components/ReaderContent.tsx @@ -42,7 +42,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i if (isInitiating.current) return; isInitiating.current = true; - const bookIds = ids || searchParams.get('ids') || ''; + const bookIds = ids || searchParams?.get('ids') || ''; const initialIds = bookIds.split(BOOK_IDS_SEPARATOR).filter(Boolean); const initialBookKeys = initialIds.map((id) => `${id}-${uniqueId()}`); setBookKeys(initialBookKeys); diff --git a/apps/readest-app/src/app/reader/hooks/useBooksManager.ts b/apps/readest-app/src/app/reader/hooks/useBooksManager.ts index d3c3e7bd..ff7268e9 100644 --- a/apps/readest-app/src/app/reader/hooks/useBooksManager.ts +++ b/apps/readest-app/src/app/reader/hooks/useBooksManager.ts @@ -21,7 +21,7 @@ const useBooksManager = () => { if (shouldUpdateSearchParams) { const ids = bookKeys.map((key) => key.split('-')[0]!); if (ids) { - navigateToReader(router, ids, searchParams.toString(), { scroll: false }); + navigateToReader(router, ids, searchParams?.toString() || '', { scroll: false }); } setShouldUpdateSearchParams(false); } diff --git a/apps/readest-app/src/components/Providers.tsx b/apps/readest-app/src/components/Providers.tsx new file mode 100644 index 00000000..643d4e6e --- /dev/null +++ b/apps/readest-app/src/components/Providers.tsx @@ -0,0 +1,16 @@ +import { AuthProvider } from '@/context/AuthContext'; +import { EnvProvider } from '@/context/EnvContext'; +import { CSPostHogProvider } from '@/context/PHContext'; +import { SyncProvider } from '@/context/SyncContext'; + +const Providers = ({ children }: { children: React.ReactNode }) => ( + + + + {children} + + + +); + +export default Providers; diff --git a/apps/readest-app/src/pages/_app.tsx b/apps/readest-app/src/pages/_app.tsx new file mode 100644 index 00000000..f3693d62 --- /dev/null +++ b/apps/readest-app/src/pages/_app.tsx @@ -0,0 +1,15 @@ +import { AppProps } from 'next/app'; +import Providers from '@/components/Providers'; + +import '../styles/globals.css'; +import '../styles/fonts.css'; + +function MyApp({ Component, pageProps }: AppProps) { + return ( + + + + ); +} + +export default MyApp; diff --git a/apps/readest-app/src/app/api/sync/route.ts b/apps/readest-app/src/pages/api/sync.ts similarity index 77% rename from apps/readest-app/src/app/api/sync/route.ts rename to apps/readest-app/src/pages/api/sync.ts index 68b38b68..e0441683 100644 --- a/apps/readest-app/src/app/api/sync/route.ts +++ b/apps/readest-app/src/pages/api/sync.ts @@ -1,3 +1,5 @@ +import Cors from 'cors'; +import type { NextApiRequest, NextApiResponse } from 'next'; import { NextRequest, NextResponse } from 'next/server'; import { PostgrestError } from '@supabase/supabase-js'; import { supabase, createSupabaseClient } from '@/utils/supabase'; @@ -207,3 +209,72 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: errorMessage }, { status: 500 }); } } + +// Helper method to wait for a middleware to execute before continuing +// And to throw an error when an error happens in a middleware +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +function runMiddleware(req: NextApiRequest, res: NextApiResponse, fn: Function) { + return new Promise((resolve, reject) => { + fn(req, res, (result: unknown) => { + if (result instanceof Error) { + return reject(result); + } + + return resolve(result); + }); + }); +} + +const cors = Cors({ + methods: ['POST', 'GET', 'HEAD'], +}); + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (!req.url) { + return res.status(400).json({ error: 'Invalid request URL' }); + } + + const protocol = process.env['PROTOCOL'] || 'http'; + const host = process.env['HOST'] || 'localhost:3000'; + const url = new URL(req.url, `${protocol}://${host}`); + + await runMiddleware(req, res, cors); + + try { + let response: Response; + + if (req.method === 'GET') { + const nextReq = new NextRequest(url.toString(), { + headers: new Headers(req.headers as Record), + method: 'GET', + }); + response = await GET(nextReq); + } else if (req.method === 'POST') { + const nextReq = new NextRequest(url.toString(), { + headers: new Headers(req.headers as Record), + method: 'POST', + body: JSON.stringify(req.body), // Ensure the body is a string + }); + response = await POST(nextReq); + } else { + res.setHeader('Allow', ['GET', 'POST']); + return res.status(405).end(`Method ${req.method} Not Allowed`); + } + + res.status(response.status); + + response.headers.forEach((value, key) => { + res.setHeader(key, value); + }); + + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + res.send(buffer); + } catch (error) { + console.error('Error processing request:', error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}; + +export default handler; diff --git a/apps/readest-app/src/pages/reader/[ids].tsx b/apps/readest-app/src/pages/reader/[ids].tsx new file mode 100644 index 00000000..5afa3287 --- /dev/null +++ b/apps/readest-app/src/pages/reader/[ids].tsx @@ -0,0 +1,22 @@ +import { useRouter } from 'next/router'; +import { AuthProvider } from '@/context/AuthContext'; +import { EnvProvider } from '@/context/EnvContext'; +import { CSPostHogProvider } from '@/context/PHContext'; +import { SyncProvider } from '@/context/SyncContext'; +import Reader from '@/app/reader/components/Reader'; + +export default function Page() { + const router = useRouter(); + const ids = router.query['ids'] as string; + return ( + + + + + + + + + + ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c85e3ea..6529a0bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + cors: + specifier: ^2.8.5 + version: 2.8.5 cssbeautify: specifier: ^0.3.1 version: 0.3.1 @@ -114,6 +117,9 @@ importers: '@tauri-apps/cli': specifier: 2.1.0 version: 2.1.0 + '@types/cors': + specifier: ^2.8.17 + version: 2.8.17 '@types/cssbeautify': specifier: ^0.3.5 version: 0.3.5 @@ -794,6 +800,9 @@ packages: '@types/common-tags@1.8.4': resolution: {integrity: sha512-S+1hLDJPjWNDhcGxsxEbepzaxWqURP/o+3cP4aa2w7yBXgdcmKGQtZzP8JbyfOd0m+33nh+8+kvxYE2UJtBDkg==} + '@types/cors@2.8.17': + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + '@types/cssbeautify@0.3.5': resolution: {integrity: sha512-bkxuJdUu7Liw14EouI8IFNvJXvd5IioAZdDvlCFBXv+Sel8uPK9rcxfvOchQKqw2AtppxRvcYaWmimi0t0Yuow==} @@ -1207,6 +1216,10 @@ packages: core-js@3.39.0: resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==} + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + cpx2@8.0.0: resolution: {integrity: sha512-RxD9jrSVNSOmfcbiPlr3XnKbUKH9K1w2HCv0skczUKhsZTueiDBecxuaSAKQkYSLQaGVA4ZQJZlTj5hVNNEvKg==} engines: {node: ^20.0.0 || >=22.0.0, npm: '>=10'} @@ -2857,6 +2870,10 @@ packages: v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + watchpack@2.4.2: resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} engines: {node: '>=10.13.0'} @@ -3467,6 +3484,10 @@ snapshots: '@types/common-tags@1.8.4': {} + '@types/cors@2.8.17': + dependencies: + '@types/node': 22.10.1 + '@types/cssbeautify@0.3.5': {} '@types/debug@4.1.12': @@ -3960,6 +3981,11 @@ snapshots: core-js@3.39.0: {} + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cpx2@8.0.0: dependencies: debounce: 2.2.0 @@ -5805,6 +5831,8 @@ snapshots: v8-compile-cache-lib@3.0.1: optional: true + vary@1.1.2: {} + watchpack@2.4.2: dependencies: glob-to-regexp: 0.4.1