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