Add CORS for the sync API

This commit is contained in:
chrox
2024-12-24 18:02:39 +01:00
parent 447eb09272
commit 17336d7ac9
13 changed files with 162 additions and 26 deletions
-3
View File
@@ -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: |
+2
View File
@@ -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",
+1 -1
View File
@@ -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",
+4 -13
View File
@@ -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 })
<meta name='twitter:description' content={description} />
<meta name='twitter:image' content={previewImage} />
</head>
<CSPostHogProvider>
<body>
<EnvProvider>
<AuthProvider>
<SyncProvider>{children}</SyncProvider>
</AuthProvider>
</EnvProvider>
</body>
</CSPostHogProvider>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
@@ -56,7 +56,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
const [selectedBooks, setSelectedBooks] = useState<string[]>([]);
const [showDeleteAlert, setShowDeleteAlert] = useState(false);
const [clickedImage, setClickedImage] = useState<string | null>(null);
const [importBookUrl] = useState(searchParams.get('url') || '');
const [importBookUrl] = useState(searchParams?.get('url') || '');
const isImportingBook = useRef(false);
const { setLibrary } = useLibraryStore();
@@ -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 <Reader ids={ids} />;
}
@@ -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);
@@ -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);
}
@@ -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 }) => (
<CSPostHogProvider>
<EnvProvider>
<AuthProvider>
<SyncProvider>{children}</SyncProvider>
</AuthProvider>
</EnvProvider>
</CSPostHogProvider>
);
export default Providers;
+15
View File
@@ -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 (
<Providers>
<Component {...pageProps} />
</Providers>
);
}
export default MyApp;
@@ -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<string, string>),
method: 'GET',
});
response = await GET(nextReq);
} else if (req.method === 'POST') {
const nextReq = new NextRequest(url.toString(), {
headers: new Headers(req.headers as Record<string, string>),
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;
@@ -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 (
<CSPostHogProvider>
<EnvProvider>
<AuthProvider>
<SyncProvider>
<Reader ids={ids} />
</SyncProvider>
</AuthProvider>
</EnvProvider>
</CSPostHogProvider>
);
}
+28
View File
@@ -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