turnstile integration for provider api

This commit is contained in:
mrjvs 2023-12-19 20:41:56 +01:00
parent 4847980947
commit b5a11ef000
9 changed files with 144 additions and 13 deletions

View File

@ -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" />

View File

@ -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
View File

@ -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:

View File

@ -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) {

View File

@ -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

View File

@ -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"]
);

View File

@ -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>

View File

@ -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())

View 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);
}}
/>
);
}