refactor progress
This commit is contained in:
108
packages/captcha/src/CaptchaProviderFactory.tsx
Normal file
108
packages/captcha/src/CaptchaProviderFactory.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {ICaptchaProvider} from '@fluxer/captcha/src/ICaptchaProvider';
|
||||
import {HcaptchaProvider} from '@fluxer/captcha/src/providers/HcaptchaProvider';
|
||||
import type {HttpCaptchaProviderOptions} from '@fluxer/captcha/src/providers/HttpCaptchaProvider';
|
||||
import type {RecaptchaProviderOptions} from '@fluxer/captcha/src/providers/RecaptchaProvider';
|
||||
import {RecaptchaProvider} from '@fluxer/captcha/src/providers/RecaptchaProvider';
|
||||
import {TestCaptchaProvider} from '@fluxer/captcha/src/providers/TestProvider';
|
||||
import {TurnstileProvider} from '@fluxer/captcha/src/providers/TurnstileProvider';
|
||||
import {UnavailableCaptchaProvider} from '@fluxer/captcha/src/providers/UnavailableCaptchaProvider';
|
||||
import type {LoggerInterface} from '@fluxer/logger/src/LoggerInterface';
|
||||
|
||||
interface BaseCaptchaProviderFactoryParams {
|
||||
logger?: LoggerInterface;
|
||||
}
|
||||
|
||||
interface CreateUnavailableCaptchaProviderParams extends BaseCaptchaProviderFactoryParams {
|
||||
mode: 'unavailable';
|
||||
}
|
||||
|
||||
interface CreateTestCaptchaProviderParams extends BaseCaptchaProviderFactoryParams {
|
||||
mode: 'test';
|
||||
}
|
||||
|
||||
interface CreateHcaptchaProviderParams extends BaseCaptchaProviderFactoryParams {
|
||||
mode: 'hcaptcha';
|
||||
secretKey: string;
|
||||
timeoutMs?: number;
|
||||
userAgent?: string;
|
||||
fetchFn?: typeof fetch;
|
||||
}
|
||||
|
||||
interface CreateTurnstileProviderParams extends BaseCaptchaProviderFactoryParams {
|
||||
mode: 'turnstile';
|
||||
secretKey: string;
|
||||
timeoutMs?: number;
|
||||
userAgent?: string;
|
||||
fetchFn?: typeof fetch;
|
||||
}
|
||||
|
||||
interface CreateRecaptchaProviderParams extends BaseCaptchaProviderFactoryParams {
|
||||
mode: 'recaptcha';
|
||||
secretKey: string;
|
||||
minimumScore?: number;
|
||||
timeoutMs?: number;
|
||||
userAgent?: string;
|
||||
fetchFn?: typeof fetch;
|
||||
}
|
||||
|
||||
export type CreateCaptchaProviderParams =
|
||||
| CreateUnavailableCaptchaProviderParams
|
||||
| CreateTestCaptchaProviderParams
|
||||
| CreateHcaptchaProviderParams
|
||||
| CreateTurnstileProviderParams
|
||||
| CreateRecaptchaProviderParams;
|
||||
|
||||
function buildHttpOptions(
|
||||
params: CreateHcaptchaProviderParams | CreateTurnstileProviderParams | CreateRecaptchaProviderParams,
|
||||
): HttpCaptchaProviderOptions {
|
||||
return {
|
||||
secretKey: params.secretKey,
|
||||
logger: params.logger,
|
||||
timeoutMs: params.timeoutMs,
|
||||
userAgent: params.userAgent,
|
||||
fetchFn: params.fetchFn,
|
||||
};
|
||||
}
|
||||
|
||||
export function createCaptchaProvider(params: CreateCaptchaProviderParams): ICaptchaProvider {
|
||||
if (params.mode === 'test') {
|
||||
return new TestCaptchaProvider();
|
||||
}
|
||||
|
||||
if (params.mode === 'hcaptcha') {
|
||||
return new HcaptchaProvider(buildHttpOptions(params));
|
||||
}
|
||||
|
||||
if (params.mode === 'turnstile') {
|
||||
return new TurnstileProvider(buildHttpOptions(params));
|
||||
}
|
||||
|
||||
if (params.mode === 'recaptcha') {
|
||||
const options: RecaptchaProviderOptions = {
|
||||
...buildHttpOptions(params),
|
||||
minimumScore: params.minimumScore,
|
||||
};
|
||||
return new RecaptchaProvider(options);
|
||||
}
|
||||
|
||||
return new UnavailableCaptchaProvider();
|
||||
}
|
||||
30
packages/captcha/src/ICaptchaProvider.tsx
Normal file
30
packages/captcha/src/ICaptchaProvider.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export interface VerifyCaptchaParams {
|
||||
token: string;
|
||||
remoteIp?: string;
|
||||
}
|
||||
|
||||
export type CaptchaProviderType = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'test' | 'unavailable';
|
||||
|
||||
export interface ICaptchaProvider {
|
||||
readonly type: CaptchaProviderType;
|
||||
verify(params: VerifyCaptchaParams): Promise<boolean>;
|
||||
}
|
||||
27
packages/captcha/src/providers/HcaptchaProvider.tsx
Normal file
27
packages/captcha/src/providers/HcaptchaProvider.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {CaptchaProviderType} from '@fluxer/captcha/src/ICaptchaProvider';
|
||||
import {HttpCaptchaProvider} from '@fluxer/captcha/src/providers/HttpCaptchaProvider';
|
||||
|
||||
export class HcaptchaProvider extends HttpCaptchaProvider {
|
||||
readonly type: CaptchaProviderType = 'hcaptcha';
|
||||
protected readonly verifyUrl = 'https://api.hcaptcha.com/siteverify';
|
||||
protected readonly providerName = 'hCaptcha';
|
||||
}
|
||||
107
packages/captcha/src/providers/HttpCaptchaProvider.tsx
Normal file
107
packages/captcha/src/providers/HttpCaptchaProvider.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {CaptchaProviderType, ICaptchaProvider, VerifyCaptchaParams} from '@fluxer/captcha/src/ICaptchaProvider';
|
||||
import type {LoggerInterface} from '@fluxer/logger/src/LoggerInterface';
|
||||
import {ms} from 'itty-time';
|
||||
|
||||
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (compatible; Fluxerbot/1.0; +https://fluxer.app)';
|
||||
const DEFAULT_TIMEOUT = ms('10 seconds');
|
||||
|
||||
export interface HttpCaptchaProviderOptions {
|
||||
secretKey: string;
|
||||
logger?: LoggerInterface;
|
||||
timeoutMs?: number;
|
||||
userAgent?: string;
|
||||
fetchFn?: typeof fetch;
|
||||
}
|
||||
|
||||
interface CaptchaVerifyResponse {
|
||||
success: boolean;
|
||||
'error-codes'?: Array<string>;
|
||||
hostname?: string;
|
||||
challenge_ts?: string;
|
||||
}
|
||||
|
||||
export abstract class HttpCaptchaProvider implements ICaptchaProvider {
|
||||
abstract readonly type: CaptchaProviderType;
|
||||
|
||||
protected readonly secretKey: string;
|
||||
protected readonly logger: LoggerInterface | undefined;
|
||||
protected readonly timeoutMs: number;
|
||||
protected readonly userAgent: string;
|
||||
protected readonly fetchFn: typeof fetch;
|
||||
|
||||
protected abstract readonly verifyUrl: string;
|
||||
protected abstract readonly providerName: string;
|
||||
|
||||
constructor(options: HttpCaptchaProviderOptions) {
|
||||
this.secretKey = options.secretKey;
|
||||
this.logger = options.logger;
|
||||
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT;
|
||||
this.userAgent = options.userAgent ?? DEFAULT_USER_AGENT;
|
||||
this.fetchFn = options.fetchFn ?? fetch;
|
||||
}
|
||||
|
||||
async verify({token, remoteIp}: VerifyCaptchaParams): Promise<boolean> {
|
||||
try {
|
||||
const body = new URLSearchParams();
|
||||
body.append('secret', this.secretKey);
|
||||
body.append('response', token);
|
||||
if (remoteIp) {
|
||||
body.append('remoteip', remoteIp);
|
||||
}
|
||||
|
||||
const response = await this.fetchFn(this.verifyUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': this.userAgent,
|
||||
},
|
||||
body: body.toString(),
|
||||
signal: AbortSignal.timeout(this.timeoutMs),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this.logger?.error({status: response.status}, `${this.providerName} verify request failed`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as CaptchaVerifyResponse;
|
||||
|
||||
if (!data.success) {
|
||||
this.logger?.warn({errorCodes: data['error-codes']}, `${this.providerName} verification failed`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.validateResponse(data);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'TimeoutError') {
|
||||
this.logger?.error({}, `${this.providerName} verification timed out after ${this.timeoutMs}ms`);
|
||||
} else {
|
||||
this.logger?.error({error}, `Error verifying ${this.providerName} token`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected validateResponse(_data: CaptchaVerifyResponse): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
58
packages/captcha/src/providers/RecaptchaProvider.tsx
Normal file
58
packages/captcha/src/providers/RecaptchaProvider.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {CaptchaProviderType} from '@fluxer/captcha/src/ICaptchaProvider';
|
||||
import type {HttpCaptchaProviderOptions} from '@fluxer/captcha/src/providers/HttpCaptchaProvider';
|
||||
import {HttpCaptchaProvider} from '@fluxer/captcha/src/providers/HttpCaptchaProvider';
|
||||
|
||||
const DEFAULT_MINIMUM_SCORE = 0.5;
|
||||
|
||||
interface RecaptchaVerifyResponse {
|
||||
success: boolean;
|
||||
'error-codes'?: Array<string>;
|
||||
score?: number;
|
||||
}
|
||||
|
||||
export interface RecaptchaProviderOptions extends HttpCaptchaProviderOptions {
|
||||
minimumScore?: number;
|
||||
}
|
||||
|
||||
export class RecaptchaProvider extends HttpCaptchaProvider {
|
||||
readonly type: CaptchaProviderType = 'recaptcha';
|
||||
protected readonly verifyUrl = 'https://www.google.com/recaptcha/api/siteverify';
|
||||
protected readonly providerName = 'reCAPTCHA';
|
||||
|
||||
private readonly minimumScore: number;
|
||||
|
||||
constructor(options: RecaptchaProviderOptions) {
|
||||
super(options);
|
||||
this.minimumScore = options.minimumScore ?? DEFAULT_MINIMUM_SCORE;
|
||||
}
|
||||
|
||||
protected override validateResponse(data: RecaptchaVerifyResponse): boolean {
|
||||
if (data.score !== undefined && data.score < this.minimumScore) {
|
||||
this.logger?.warn(
|
||||
{score: data.score, minimumScore: this.minimumScore},
|
||||
'reCAPTCHA score below minimum threshold',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
28
packages/captcha/src/providers/TestProvider.tsx
Normal file
28
packages/captcha/src/providers/TestProvider.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {CaptchaProviderType, ICaptchaProvider, VerifyCaptchaParams} from '@fluxer/captcha/src/ICaptchaProvider';
|
||||
|
||||
export class TestCaptchaProvider implements ICaptchaProvider {
|
||||
readonly type: CaptchaProviderType = 'test';
|
||||
|
||||
async verify(_params: VerifyCaptchaParams): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
27
packages/captcha/src/providers/TurnstileProvider.tsx
Normal file
27
packages/captcha/src/providers/TurnstileProvider.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {CaptchaProviderType} from '@fluxer/captcha/src/ICaptchaProvider';
|
||||
import {HttpCaptchaProvider} from '@fluxer/captcha/src/providers/HttpCaptchaProvider';
|
||||
|
||||
export class TurnstileProvider extends HttpCaptchaProvider {
|
||||
readonly type: CaptchaProviderType = 'turnstile';
|
||||
protected readonly verifyUrl = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
|
||||
protected readonly providerName = 'Turnstile';
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {CaptchaProviderType, ICaptchaProvider, VerifyCaptchaParams} from '@fluxer/captcha/src/ICaptchaProvider';
|
||||
|
||||
export class UnavailableCaptchaProvider implements ICaptchaProvider {
|
||||
readonly type: CaptchaProviderType = 'unavailable';
|
||||
|
||||
async verify(_params: VerifyCaptchaParams): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user