diff --git a/app/backend/src/lib/captcha.ts b/app/backend/src/lib/captcha.ts
new file mode 100644
index 0000000..6c252c5
--- /dev/null
+++ b/app/backend/src/lib/captcha.ts
@@ -0,0 +1,71 @@
+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 += ``;
+ }
+
+ 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 += `${char}`;
+ }
+
+ return ``;
+}
+
+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 {
+ const key = CAPTCHA_PREFIX + captchaId;
+ const stored = await redis.get(key);
+
+ if (!stored) return false;
+
+ await redis.del(key);
+
+ return stored === userSolution.toLowerCase().trim();
+}