mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 09:25:08 +10:00
chore: merge main, resolve biome formatting conflicts
Merge origin/main into feat/external-2fa-codes. Resolve formatting conflicts caused by biome rollout; preserve both feature streams: PR's external 2FA token + signing-session 2FA proof additions plus main's RateLimit/RecipientExpired/signingReminders/date-auto-insert. In complete-document-with-token.ts, drop the duplicate early field-fetching block introduced when main moved that logic later with date auto-insert support; keep the EXTERNAL_TWO_FACTOR_AUTH check using derivedRecipientActionAuth.
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
const TURNSTILE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
|
||||
|
||||
type TurnstileVerifyResponse = {
|
||||
success: boolean;
|
||||
'error-codes': string[];
|
||||
challenge_ts?: string;
|
||||
hostname?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify a captcha token server-side.
|
||||
*
|
||||
* Currently supports Cloudflare Turnstile. This is a no-op if
|
||||
* `NEXT_PRIVATE_TURNSTILE_SECRET_KEY` is not configured, making captcha
|
||||
* verification an opt-in feature.
|
||||
*/
|
||||
export const verifyCaptchaToken = async ({
|
||||
token,
|
||||
ipAddress,
|
||||
}: {
|
||||
token?: string | null;
|
||||
ipAddress?: string | null;
|
||||
}) => {
|
||||
const secretKey = process.env.NEXT_PRIVATE_TURNSTILE_SECRET_KEY;
|
||||
|
||||
// If no secret key is configured, skip verification.
|
||||
if (!secretKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
logger.warn({
|
||||
msg: 'Captcha verification rejected: missing token',
|
||||
ipAddress,
|
||||
});
|
||||
|
||||
throw new AppError(AppErrorCode.INVALID_CAPTCHA, {
|
||||
message: 'Captcha token is required',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const formData = new URLSearchParams();
|
||||
|
||||
formData.append('secret', secretKey);
|
||||
formData.append('response', token);
|
||||
|
||||
if (ipAddress) {
|
||||
formData.append('remoteip', ipAddress);
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(TURNSTILE_VERIFY_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formData.toString(),
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error({
|
||||
msg: 'Captcha verification failed: network error calling siteverify',
|
||||
err,
|
||||
ipAddress,
|
||||
});
|
||||
|
||||
throw new AppError(AppErrorCode.INVALID_CAPTCHA, {
|
||||
message: 'Captcha verification failed',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error({
|
||||
msg: 'Captcha verification failed: non-2xx response from siteverify',
|
||||
status: response.status,
|
||||
ipAddress,
|
||||
});
|
||||
|
||||
throw new AppError(AppErrorCode.INVALID_CAPTCHA, {
|
||||
message: `Captcha verification request failed with status ${response.status}`,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const result: TurnstileVerifyResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
logger.warn({
|
||||
msg: 'Captcha verification rejected by provider',
|
||||
errorCodes: result['error-codes'],
|
||||
hostname: result.hostname,
|
||||
ipAddress,
|
||||
});
|
||||
|
||||
throw new AppError(AppErrorCode.INVALID_CAPTCHA, {
|
||||
message: `Captcha verification failed: ${result['error-codes']?.join(', ') ?? 'unknown'}`,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user