mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-25 18:21:53 +01:00
turnstile integration for provider api
This commit is contained in:
parent
4847980947
commit
b5a11ef000
@ -20,7 +20,7 @@
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
|
||||
<script src="/config.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/gh/movie-web/6C6F6C7A@8b821f445b83d51ef1b8f42c99b7346f6b47dce5/out.js"></script>
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
|
||||
|
||||
<!-- prevent darkreader extension from messing with our already dark site -->
|
||||
<meta name="darkreader-lock" />
|
||||
@ -59,4 +59,4 @@
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
@ -57,6 +57,7 @@
|
||||
"react-i18next": "^12.1.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-sticky-el": "^2.1.0",
|
||||
"react-turnstile": "^1.1.2",
|
||||
"react-use": "^17.4.0",
|
||||
"slugify": "^1.6.6",
|
||||
"subsrt-ts": "^2.1.1",
|
||||
|
13
pnpm-lock.yaml
generated
13
pnpm-lock.yaml
generated
@ -104,6 +104,9 @@ dependencies:
|
||||
react-sticky-el:
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0(react-dom@17.0.2)(react@17.0.2)
|
||||
react-turnstile:
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2(react-dom@17.0.2)(react@17.0.2)
|
||||
react-use:
|
||||
specifier: ^17.4.0
|
||||
version: 17.4.0(react-dom@17.0.2)(react@17.0.2)
|
||||
@ -5321,6 +5324,16 @@ packages:
|
||||
react-dom: 17.0.2(react@17.0.2)
|
||||
dev: false
|
||||
|
||||
/react-turnstile@1.1.2(react-dom@17.0.2)(react@17.0.2):
|
||||
resolution: {integrity: sha512-wfhSf4JtXlmLRkfxMryU8yEeCbh401muKoInhx+TegYwP8RprUW5XPZa8WnCNZiYpMy1i6IXAb1Ar7xj5HxJag==}
|
||||
peerDependencies:
|
||||
react: '>= 17.0.0'
|
||||
react-dom: '>= 17.0.0'
|
||||
dependencies:
|
||||
react: 17.0.2
|
||||
react-dom: 17.0.2(react@17.0.2)
|
||||
dev: false
|
||||
|
||||
/react-universal-interface@0.6.2(react@17.0.2)(tslib@2.6.2):
|
||||
resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==}
|
||||
peerDependencies:
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { MetaOutput, NotFoundError, ScrapeMedia } from "@movie-web/providers";
|
||||
|
||||
import { mwFetch } from "@/backend/helpers/fetch";
|
||||
import { getTurnstileToken, isTurnstileInitialized } from "@/stores/turnstile";
|
||||
|
||||
let metaDataCache: MetaOutput[] | null = null;
|
||||
let token: null | string = null;
|
||||
|
||||
export function setCachedMetadata(data: MetaOutput[]) {
|
||||
metaDataCache = data;
|
||||
@ -12,6 +14,20 @@ export function getCachedMetadata(): MetaOutput[] {
|
||||
return metaDataCache ?? [];
|
||||
}
|
||||
|
||||
function getTokenIfValid(): null | string {
|
||||
if (!token) return null;
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) return null;
|
||||
try {
|
||||
const parsedData = JSON.parse(atob(parts[2]));
|
||||
if (!parsedData.exp) return token;
|
||||
if (Date.now() < parsedData.exp) return token;
|
||||
} catch {
|
||||
// we dont care about parse errors
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function fetchMetadata(base: string) {
|
||||
if (metaDataCache) return;
|
||||
const data = await mwFetch<MetaOutput[][]>(`${base}/metadata`);
|
||||
@ -67,8 +83,21 @@ export function makeProviderUrl(base: string) {
|
||||
};
|
||||
}
|
||||
|
||||
export function connectServerSideEvents<T>(url: string, endEvents: string[]) {
|
||||
const eventSource = new EventSource(url);
|
||||
export async function connectServerSideEvents<T>(
|
||||
url: string,
|
||||
endEvents: string[]
|
||||
) {
|
||||
// fetch token to use
|
||||
let apiToken = getTokenIfValid();
|
||||
if (!apiToken && isTurnstileInitialized()) {
|
||||
apiToken = await getTurnstileToken();
|
||||
}
|
||||
|
||||
// insert token, if its set
|
||||
const parsedUrl = new URL(url);
|
||||
if (apiToken) parsedUrl.searchParams.set("token", apiToken);
|
||||
const eventSource = new EventSource(parsedUrl.toString());
|
||||
|
||||
let promReject: (reason?: any) => void;
|
||||
let promResolve: (value: T) => void;
|
||||
const promise = new Promise<T>((resolve, reject) => {
|
||||
@ -83,6 +112,10 @@ export function connectServerSideEvents<T>(url: string, endEvents: string[]) {
|
||||
});
|
||||
});
|
||||
|
||||
eventSource.addEventListener("token", (e) => {
|
||||
token = JSON.parse(e.data);
|
||||
});
|
||||
|
||||
eventSource.addEventListener("error", (err: MessageEvent<any>) => {
|
||||
eventSource.close();
|
||||
if (err.data) {
|
||||
|
@ -41,7 +41,7 @@ export function useEmbedScraping(
|
||||
try {
|
||||
if (providerApiUrl) {
|
||||
const baseUrlMaker = makeProviderUrl(providerApiUrl);
|
||||
const conn = connectServerSideEvents<EmbedOutput>(
|
||||
const conn = await connectServerSideEvents<EmbedOutput>(
|
||||
baseUrlMaker.scrapeEmbed(embedId, url),
|
||||
["completed", "noOutput"]
|
||||
);
|
||||
@ -105,7 +105,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
|
||||
try {
|
||||
if (providerApiUrl) {
|
||||
const baseUrlMaker = makeProviderUrl(providerApiUrl);
|
||||
const conn = connectServerSideEvents<SourcererOutput>(
|
||||
const conn = await connectServerSideEvents<SourcererOutput>(
|
||||
baseUrlMaker.scrapeSource(sourceId, scrapeMedia),
|
||||
["completed", "noOutput"]
|
||||
);
|
||||
@ -146,7 +146,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
|
||||
try {
|
||||
if (providerApiUrl) {
|
||||
const baseUrlMaker = makeProviderUrl(providerApiUrl);
|
||||
const conn = connectServerSideEvents<EmbedOutput>(
|
||||
const conn = await connectServerSideEvents<EmbedOutput>(
|
||||
baseUrlMaker.scrapeEmbed(
|
||||
result.embeds[0].embedId,
|
||||
result.embeds[0].url
|
||||
|
@ -156,7 +156,7 @@ export function useScrape() {
|
||||
if (providerApiUrl) {
|
||||
startScrape();
|
||||
const baseUrlMaker = makeProviderUrl(providerApiUrl);
|
||||
const conn = connectServerSideEvents<RunOutput | "">(
|
||||
const conn = await connectServerSideEvents<RunOutput | "">(
|
||||
baseUrlMaker.scrapeAll(media),
|
||||
["completed", "noOutput"]
|
||||
);
|
||||
|
@ -10,6 +10,7 @@ import ReactDOM from "react-dom";
|
||||
import { HelmetProvider } from "react-helmet-async";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BrowserRouter, HashRouter } from "react-router-dom";
|
||||
import Turnstile from "react-turnstile";
|
||||
import { useAsync } from "react-use";
|
||||
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
@ -30,16 +31,12 @@ import { useLanguageStore } from "@/stores/language";
|
||||
import { ProgressSyncer } from "@/stores/progress/ProgressSyncer";
|
||||
import { SettingsSyncer } from "@/stores/subtitles/SettingsSyncer";
|
||||
import { ThemeProvider } from "@/stores/theme";
|
||||
import { TurnstileProvider } from "@/stores/turnstile";
|
||||
|
||||
import { initializeChromecast } from "./setup/chromecast";
|
||||
import { initializeOldStores } from "./stores/__old/migrations";
|
||||
|
||||
// initialize
|
||||
const key =
|
||||
(window as any)?.__CONFIG__?.VITE_KEY ?? import.meta.env.VITE_KEY ?? null;
|
||||
if (key) {
|
||||
(window as any).initMW(conf().PROXY_URLS, key);
|
||||
}
|
||||
initializeChromecast();
|
||||
|
||||
function LoadingScreen(props: { type: "user" | "lazy" }) {
|
||||
@ -148,6 +145,7 @@ function TheRouter(props: { children: ReactNode }) {
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<ErrorBoundary>
|
||||
<TurnstileProvider />
|
||||
<HelmetProvider>
|
||||
<Suspense fallback={<LoadingScreen type="lazy" />}>
|
||||
<ThemeProvider applyGlobal>
|
||||
|
@ -17,6 +17,7 @@ interface Config {
|
||||
NORMAL_ROUTER: boolean;
|
||||
BACKEND_URL: string;
|
||||
DISALLOWED_IDS: string;
|
||||
TURNSTILE_KEY: string;
|
||||
}
|
||||
|
||||
export interface RuntimeConfig {
|
||||
@ -30,6 +31,7 @@ export interface RuntimeConfig {
|
||||
PROXY_URLS: string[];
|
||||
BACKEND_URL: string;
|
||||
DISALLOWED_IDS: string[];
|
||||
TURNSTILE_KEY: string | null;
|
||||
}
|
||||
|
||||
const env: Record<keyof Config, undefined | string> = {
|
||||
@ -43,6 +45,7 @@ const env: Record<keyof Config, undefined | string> = {
|
||||
NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER,
|
||||
BACKEND_URL: import.meta.env.VITE_BACKEND_URL,
|
||||
DISALLOWED_IDS: import.meta.env.VITE_DISALLOWED_IDS,
|
||||
TURNSTILE_KEY: import.meta.env.VITE_TURNSTILE_KEY,
|
||||
};
|
||||
|
||||
// loads from different locations, in order: environment (VITE_{KEY}), window (public/config.js)
|
||||
@ -63,6 +66,7 @@ function getKey(key: keyof Config, defaultString?: string): string {
|
||||
|
||||
export function conf(): RuntimeConfig {
|
||||
const dmcaEmail = getKey("DMCA_EMAIL");
|
||||
const turnstileKey = getKey("TURNSTILE_KEY");
|
||||
return {
|
||||
APP_VERSION,
|
||||
GITHUB_LINK,
|
||||
@ -75,6 +79,7 @@ export function conf(): RuntimeConfig {
|
||||
.split(",")
|
||||
.map((v) => v.trim()),
|
||||
NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true",
|
||||
TURNSTILE_KEY: turnstileKey.length > 0 ? turnstileKey : null,
|
||||
DISALLOWED_IDS: getKey("DISALLOWED_IDS", "")
|
||||
.split(",")
|
||||
.map((v) => v.trim())
|
||||
|
81
src/stores/turnstile/index.tsx
Normal file
81
src/stores/turnstile/index.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import Turnstile, { BoundTurnstileObject } from "react-turnstile";
|
||||
import { create } from "zustand";
|
||||
import { immer } from "zustand/middleware/immer";
|
||||
|
||||
import { conf } from "@/setup/config";
|
||||
|
||||
export interface TurnstileStore {
|
||||
turnstile: BoundTurnstileObject | null;
|
||||
cbs: ((token: string | null) => void)[];
|
||||
setTurnstile(v: BoundTurnstileObject | null): void;
|
||||
getToken(): Promise<string>;
|
||||
processToken(token: string | null): void;
|
||||
}
|
||||
|
||||
export const useTurnstileStore = create(
|
||||
immer<TurnstileStore>((set, get) => ({
|
||||
turnstile: null,
|
||||
cbs: [],
|
||||
processToken(token) {
|
||||
const cbs = get().cbs;
|
||||
cbs.forEach((fn) => fn(token));
|
||||
set((s) => {
|
||||
s.cbs = [];
|
||||
});
|
||||
},
|
||||
getToken() {
|
||||
return new Promise((resolve, reject) => {
|
||||
set((s) => {
|
||||
s.cbs = [
|
||||
...s.cbs,
|
||||
(token) => {
|
||||
if (!token) reject(new Error("Failed to get token"));
|
||||
else resolve(token);
|
||||
},
|
||||
];
|
||||
});
|
||||
});
|
||||
},
|
||||
setTurnstile(v) {
|
||||
set((s) => {
|
||||
s.turnstile = v;
|
||||
});
|
||||
},
|
||||
}))
|
||||
);
|
||||
|
||||
export function getTurnstile() {
|
||||
return useTurnstileStore.getState().turnstile;
|
||||
}
|
||||
|
||||
export function isTurnstileInitialized() {
|
||||
return !!getTurnstile();
|
||||
}
|
||||
|
||||
export function getTurnstileToken() {
|
||||
const turnstile = getTurnstile();
|
||||
turnstile?.reset();
|
||||
turnstile?.execute();
|
||||
return useTurnstileStore.getState().getToken();
|
||||
}
|
||||
|
||||
export function TurnstileProvider() {
|
||||
const siteKey = conf().TURNSTILE_KEY;
|
||||
const setTurnstile = useTurnstileStore((s) => s.setTurnstile);
|
||||
const processToken = useTurnstileStore((s) => s.processToken);
|
||||
if (!siteKey) return null;
|
||||
return (
|
||||
<Turnstile
|
||||
sitekey={siteKey}
|
||||
onLoad={(_widgetId, bound) => {
|
||||
setTurnstile(bound);
|
||||
}}
|
||||
onError={() => {
|
||||
processToken(null);
|
||||
}}
|
||||
onVerify={(token) => {
|
||||
processToken(token);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user