using Microsoft.AspNetCore.DataProtection; namespace JobsMedical.Web.Services; /// /// A built-in, stateless math CAPTCHA (no Google reCAPTCHA — it's blocked in Iran). It renders a /// simple "a + b = ?" question plus a tamper-proof token (the answer + timestamp, data-protected). /// Verification unprotects the token, checks it hasn't expired, and compares the answer. Because /// the answer is encrypted in the token (not guessable/forgeable) and no server session is needed, /// it works across the load-balanced/containerized deploy with zero extra storage. /// public class CaptchaService { private readonly IDataProtector _protector; private static readonly TimeSpan Ttl = TimeSpan.FromMinutes(10); public CaptchaService(IDataProtectionProvider dp) => _protector = dp.CreateProtector("hamkadr.captcha.v1"); /// Returns the question to show (e.g. "۳ + ۵") and an opaque token for a hidden field. public (string Question, string Token) Create() { var a = Random.Shared.Next(1, 10); var b = Random.Shared.Next(1, 10); var token = _protector.Protect($"{a + b}|{DateTime.UtcNow.Ticks}"); return ($"{ToPersian(a)} + {ToPersian(b)}", token); } public bool Verify(string? token, string? answer) { if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(answer)) return false; try { var parts = _protector.Unprotect(token).Split('|'); if (parts.Length != 2) return false; if (long.TryParse(parts[1], out var ticks)) { var issued = new DateTime(ticks, DateTimeKind.Utc); if (DateTime.UtcNow - issued > Ttl) return false; // expired } return parts[0] == ToLatin(answer).Trim(); } catch { return false; } // tampered / undecryptable } private static string ToPersian(int n) => new string(n.ToString().Select(c => (char)('۰' + (c - '0'))).ToArray()); private static string ToLatin(string s) { var chars = s.ToCharArray(); for (var i = 0; i < chars.Length; i++) { if (chars[i] >= '۰' && chars[i] <= '۹') chars[i] = (char)('0' + (chars[i] - '۰')); else if (chars[i] >= '٠' && chars[i] <= '٩') chars[i] = (char)('0' + (chars[i] - '٠')); } return new string(chars); } }