Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
72 lines
2.4 KiB
TypeScript
72 lines
2.4 KiB
TypeScript
import { randomBytes } from 'node:crypto';
|
|
import { redis } from '../redis.js';
|
|
|
|
const CAPTCHA_PREFIX = 'captcha:';
|
|
const CAPTCHA_TTL = 120;
|
|
|
|
function generateCaptchaText(length: number = 5): string {
|
|
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
let result = '';
|
|
for (let i = 0; i < length; i++) {
|
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function generateCaptchaSVG(text: string): string {
|
|
const width = 200;
|
|
const height = 80;
|
|
const colors = ['#0F6B73', '#D49A2A', '#C76D4A', '#2F8F6B', '#5F6B6D'];
|
|
const bgColor = '#f0f0f0';
|
|
|
|
let noiseSVG = '';
|
|
for (let i = 0; i < 5; i++) {
|
|
const x1 = Math.random() * width;
|
|
const y1 = Math.random() * height;
|
|
const x2 = Math.random() * width;
|
|
const y2 = Math.random() * height;
|
|
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
noiseSVG += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${color}" stroke-width="1.5" opacity="0.3"/>`;
|
|
}
|
|
|
|
let textSVG = '';
|
|
const charSpacing = width / (text.length + 1);
|
|
|
|
for (let i = 0; i < text.length; i++) {
|
|
const char = text[i];
|
|
const x = charSpacing * (i + 1) + (Math.random() - 0.5) * 10;
|
|
const y = height / 2 + (Math.random() - 0.5) * 15;
|
|
const rotation = (Math.random() - 0.5) * 30;
|
|
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
const fontSize = 32 + Math.random() * 8;
|
|
textSVG += `<text x="${x}" y="${y}" font-size="${fontSize}" font-weight="bold" fill="${color}" transform="rotate(${rotation} ${x} ${y})" font-family="Arial, sans-serif">${char}</text>`;
|
|
}
|
|
|
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
|
<rect width="${width}" height="${height}" fill="${bgColor}"/>
|
|
${noiseSVG}
|
|
${textSVG}
|
|
</svg>`;
|
|
}
|
|
|
|
export async function createCaptcha() {
|
|
const id = randomBytes(16).toString('hex');
|
|
const text = generateCaptchaText(5);
|
|
const svg = generateCaptchaSVG(text);
|
|
|
|
await redis.set(CAPTCHA_PREFIX + id, text.toLowerCase(), 'EX', CAPTCHA_TTL);
|
|
|
|
return { id, data: svg };
|
|
}
|
|
|
|
export async function verifyCaptcha(captchaId: string, userSolution: string): Promise<boolean> {
|
|
const key = CAPTCHA_PREFIX + captchaId;
|
|
const stored = await redis.get(key);
|
|
|
|
if (!stored) return false;
|
|
|
|
await redis.del(key);
|
|
|
|
return stored === userSolution.toLowerCase().trim();
|
|
}
|