port FriendlyCaptcha to the new captcha infrastructure

This commit is contained in:
Hazelnoot 2025-02-03 18:17:12 -05:00
parent b5ff784b1c
commit 5781e99861

View file

@ -11,7 +11,7 @@ import { MiMeta } from '@/models/Meta.js';
import Logger from '@/logger.js'; import Logger from '@/logger.js';
import { LoggerService } from './LoggerService.js'; import { LoggerService } from './LoggerService.js';
export const supportedCaptchaProviders = ['none', 'hcaptcha', 'mcaptcha', 'recaptcha', 'turnstile', 'testcaptcha'] as const; export const supportedCaptchaProviders = ['none', 'hcaptcha', 'mcaptcha', 'recaptcha', 'turnstile', 'fc', 'testcaptcha'] as const;
export type CaptchaProvider = typeof supportedCaptchaProviders[number]; export type CaptchaProvider = typeof supportedCaptchaProviders[number];
export const captchaErrorCodes = { export const captchaErrorCodes = {
@ -43,6 +43,10 @@ export type CaptchaSetting = {
siteKey: string | null; siteKey: string | null;
secretKey: string | null; secretKey: string | null;
} }
fc: {
siteKey: string | null;
secretKey: string | null;
}
} }
export class CaptchaError extends Error { export class CaptchaError extends Error {
@ -141,7 +145,7 @@ export class CaptchaService {
@bindThis @bindThis
public async verifyFriendlyCaptcha(secret: string, response: string | null | undefined): Promise<void> { public async verifyFriendlyCaptcha(secret: string, response: string | null | undefined): Promise<void> {
if (response == null) { if (response == null) {
throw new Error('frc-failed: no response provided'); throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'frc-failed: no response provided');
} }
const result = await this.httpRequestService.send('https://api.friendlycaptcha.com/api/v1/siteverify', { const result = await this.httpRequestService.send('https://api.friendlycaptcha.com/api/v1/siteverify', {
@ -153,17 +157,17 @@ export class CaptchaService {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}); }, { throwErrorWhenResponseNotOk: false });
if (result.status !== 200) { if (result.status !== 200) {
throw new Error('frc-failed: frc didn\'t return 200 OK'); throw new CaptchaError(captchaErrorCodes.requestFailed, `frc-request-failed: ${result.status}`);
} }
const resp = await result.json() as CaptchaResponse; const resp = await result.json() as CaptchaResponse;
if (resp.success !== true) { if (resp.success !== true) {
const errorCodes = resp['errors'] ? resp['errors'].join(', ') : ''; const errorCodes = resp['errors'] ? resp['errors'].join(', ') : '';
throw new Error(`frc-failed: ${errorCodes}`); throw new CaptchaError(captchaErrorCodes.verificationFailed, `frc-failed: ${errorCodes}`);
} }
} }
@ -253,6 +257,10 @@ export class CaptchaService {
provider = 'testcaptcha'; provider = 'testcaptcha';
break; break;
} }
case meta.enableFC: {
provider = 'fc';
break;
}
default: { default: {
provider = 'none'; provider = 'none';
break; break;
@ -278,6 +286,10 @@ export class CaptchaService {
siteKey: meta.turnstileSiteKey, siteKey: meta.turnstileSiteKey,
secretKey: meta.turnstileSecretKey, secretKey: meta.turnstileSecretKey,
}, },
fc: {
siteKey: meta.fcSiteKey,
secretKey: meta.fcSecretKey,
},
}; };
} }
@ -358,6 +370,14 @@ export class CaptchaService {
await this.verifyTestcaptcha(params.captchaResult); await this.verifyTestcaptcha(params.captchaResult);
await this.updateMeta(provider, params); await this.updateMeta(provider, params);
}, },
fc: async () => {
if (!params?.secret || !params.captchaResult) {
throw new CaptchaError(captchaErrorCodes.invalidParameters, 'frc-failed: secret and captureResult are required');
}
await this.verifyFriendlyCaptcha(params.captchaResult, params.captchaResult);
await this.updateMeta(provider, params);
},
}[provider]; }[provider];
return operation() return operation()
@ -390,7 +410,7 @@ export class CaptchaService {
('enableMcaptcha' | 'mcaptchaSitekey' | 'mcaptchaSecretKey' | 'mcaptchaInstanceUrl') | ('enableMcaptcha' | 'mcaptchaSitekey' | 'mcaptchaSecretKey' | 'mcaptchaInstanceUrl') |
('enableRecaptcha' | 'recaptchaSiteKey' | 'recaptchaSecretKey') | ('enableRecaptcha' | 'recaptchaSiteKey' | 'recaptchaSecretKey') |
('enableTurnstile' | 'turnstileSiteKey' | 'turnstileSecretKey') | ('enableTurnstile' | 'turnstileSiteKey' | 'turnstileSecretKey') |
('enableTestcaptcha') ('enableTestcaptcha' | 'enableFC' | 'fcSiteKey' | 'fcSecretKey')
> >
> = { > = {
enableHcaptcha: provider === 'hcaptcha', enableHcaptcha: provider === 'hcaptcha',
@ -398,6 +418,7 @@ export class CaptchaService {
enableRecaptcha: provider === 'recaptcha', enableRecaptcha: provider === 'recaptcha',
enableTurnstile: provider === 'turnstile', enableTurnstile: provider === 'turnstile',
enableTestcaptcha: provider === 'testcaptcha', enableTestcaptcha: provider === 'testcaptcha',
enableFC: provider === 'fc',
}; };
const updateIfNotUndefined = <K extends keyof typeof metaPartial>(key: K, value: typeof metaPartial[K]) => { const updateIfNotUndefined = <K extends keyof typeof metaPartial>(key: K, value: typeof metaPartial[K]) => {
@ -427,6 +448,10 @@ export class CaptchaService {
updateIfNotUndefined('turnstileSecretKey', params?.secret); updateIfNotUndefined('turnstileSecretKey', params?.secret);
break; break;
} }
case 'fc': {
updateIfNotUndefined('fcSiteKey', params?.sitekey);
updateIfNotUndefined('fcSecretKey', params?.secret);
}
} }
await this.metaService.update(metaPartial); await this.metaService.update(metaPartial);