feat: implement enhanced captcha system
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { generateCaptcha, validateCaptcha, CaptchaResult } from './captcha';
|
||||
|
||||
describe('Enhanced Captcha System', () => {
|
||||
test('should generate captcha with medium complexity', () => {
|
||||
const result = generateCaptcha('medium');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.question).toBeDefined();
|
||||
expect(result.answer).toBeDefined();
|
||||
expect(result.hash).toBeDefined();
|
||||
expect(result.timestamp).toBeDefined();
|
||||
expect(typeof result.question).toBe('string');
|
||||
expect(typeof result.answer).toBe('number');
|
||||
});
|
||||
|
||||
test('should generate different captcha each time', () => {
|
||||
const result1 = generateCaptcha('medium');
|
||||
const result2 = generateCaptcha('medium');
|
||||
expect(result1.hash).not.toBe(result2.hash);
|
||||
});
|
||||
|
||||
test('should validate correct captcha answer', () => {
|
||||
const captcha = generateCaptcha('medium');
|
||||
const isValid = validateCaptcha(captcha.hash, captcha.answer, captcha.timestamp);
|
||||
expect(isValid).toBe(true);
|
||||
});
|
||||
|
||||
test('should reject incorrect captcha answer', () => {
|
||||
const captcha = generateCaptcha('medium');
|
||||
const isValid = validateCaptcha(captcha.hash, captcha.answer + 1, captcha.timestamp);
|
||||
expect(isValid).toBe(false);
|
||||
});
|
||||
|
||||
test('should reject expired captcha', () => {
|
||||
const captcha = generateCaptcha('medium');
|
||||
const expiredTimestamp = Date.now() - (6 * 60 * 1000); // 6 minutes ago
|
||||
const isValid = validateCaptcha(captcha.hash, captcha.answer, expiredTimestamp);
|
||||
expect(isValid).toBe(false);
|
||||
});
|
||||
|
||||
test('should generate complex captcha', () => {
|
||||
const result = generateCaptcha('complex');
|
||||
expect(result).toBeDefined();
|
||||
const numbers = result.question.match(/\d+/g);
|
||||
expect(numbers?.length).toBeGreaterThan(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import { getSecurityConfig } from './config';
|
||||
|
||||
export interface CaptchaResult {
|
||||
question: string;
|
||||
answer: number;
|
||||
hash: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
function generateSimpleCaptcha(): CaptchaResult {
|
||||
const num1 = Math.floor(Math.random() * 10) + 1;
|
||||
const num2 = Math.floor(Math.random() * 10) + 1;
|
||||
const answer = num1 + num2;
|
||||
const question = `${num1} + ${num2} = ?`;
|
||||
|
||||
return {
|
||||
question,
|
||||
answer,
|
||||
hash: '',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function generateMediumCaptcha(): CaptchaResult {
|
||||
const operations = ['+', '-', '*'];
|
||||
const operation = operations[Math.floor(Math.random() * operations.length)];
|
||||
const num1 = Math.floor(Math.random() * 20) + 1;
|
||||
const num2 = Math.floor(Math.random() * 10) + 1;
|
||||
|
||||
let answer: number;
|
||||
let question: string;
|
||||
|
||||
switch (operation) {
|
||||
case '+':
|
||||
answer = num1 + num2;
|
||||
question = `${num1} + ${num2} = ?`;
|
||||
break;
|
||||
case '-':
|
||||
answer = num1 - num2;
|
||||
question = `${num1} - ${num2} = ?`;
|
||||
break;
|
||||
case '*':
|
||||
answer = num1 * num2;
|
||||
question = `${num1} * ${num2} = ?`;
|
||||
break;
|
||||
default:
|
||||
answer = num1 + num2;
|
||||
question = `${num1} + ${num2} = ?`;
|
||||
}
|
||||
|
||||
return {
|
||||
question,
|
||||
answer,
|
||||
hash: '',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function generateComplexCaptcha(): CaptchaResult {
|
||||
const num1 = Math.floor(Math.random() * 15) + 1;
|
||||
const num2 = Math.floor(Math.random() * 10) + 1;
|
||||
const num3 = Math.floor(Math.random() * 5) + 1;
|
||||
|
||||
const operations = ['+', '-', '*'];
|
||||
const op1 = operations[Math.floor(Math.random() * operations.length)];
|
||||
const op2 = operations[Math.floor(Math.random() * operations.length)];
|
||||
|
||||
let answer: number;
|
||||
let question: string;
|
||||
|
||||
const temp1 = op1 === '+' ? num1 + num2 : op1 === '-' ? num1 - num2 : num1 * num2;
|
||||
answer = op2 === '+' ? temp1 + num3 : op2 === '-' ? temp1 - num3 : temp1 * num3;
|
||||
|
||||
question = `(${num1} ${op1} ${num2}) ${op2} ${num3} = ?`;
|
||||
|
||||
return {
|
||||
question,
|
||||
answer,
|
||||
hash: '',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function generateHash(answer: number, timestamp: number): string {
|
||||
const secret = process.env.CAPTCHA_SECRET || 'default-secret-key';
|
||||
const data = `${answer}-${timestamp}-${secret}`;
|
||||
return Buffer.from(data).toString('base64');
|
||||
}
|
||||
|
||||
export function generateCaptcha(complexity: 'simple' | 'medium' | 'complex' = 'medium'): CaptchaResult {
|
||||
let captcha: CaptchaResult;
|
||||
|
||||
switch (complexity) {
|
||||
case 'simple':
|
||||
captcha = generateSimpleCaptcha();
|
||||
break;
|
||||
case 'complex':
|
||||
captcha = generateComplexCaptcha();
|
||||
break;
|
||||
case 'medium':
|
||||
default:
|
||||
captcha = generateMediumCaptcha();
|
||||
break;
|
||||
}
|
||||
|
||||
captcha.hash = generateHash(captcha.answer, captcha.timestamp);
|
||||
return captcha;
|
||||
}
|
||||
|
||||
export function validateCaptcha(hash: string, answer: number, timestamp: number): boolean {
|
||||
const config = getSecurityConfig();
|
||||
const now = Date.now();
|
||||
const ageMinutes = (now - timestamp) / (1000 * 60);
|
||||
|
||||
if (ageMinutes > config.captcha.expiryMinutes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expectedHash = generateHash(answer, timestamp);
|
||||
return hash === expectedHash;
|
||||
}
|
||||
Reference in New Issue
Block a user