Files
modstagram-back/utils/phoneNumberMasking.js
payacom 26979057d7 feat: mask Iranian phone numbers in user-generated text fields
کاربران نتونن با گذاشتن شماره موبایل/تلفن توی بیو، کپشن پست/ریلز، توضیحاتِ
فروشگاه/کالا/پروژه/دوره/پروفایلِ آموزشگاه، بیرون از پلتفرم تماس بدن. یک
util مشترک (utils/phoneNumberMasking.js) شماره‌های ایرانی رو حتی وقتی
رقم‌ها با فاصله/خط‌فاصله/نقطه از هم جدا شده باشن تشخیص می‌ده و فقط همون
بخش رو با * جایگزین می‌کنه — بدون اینکه به قیمت/سایز/کد سفارش و بقیه‌ی
اعدادِ عادی دست بزنه. موقعِ ذخیره (نه نمایش) اعمال می‌شه، پس هیچ endpointِ
خوندنی نیاز به تغییر نداره.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 15:54:26 +03:30

82 lines
3.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Masks Iranian mobile/landline phone numbers inside free-text fields (bio, captions,
* descriptions, ...) so users can't share off-platform contact numbers through them.
* Applied at write time (before saving), not at read time — so the raw number never
* even reaches storage.
*
* Must survive users spacing digits out to dodge a naive "11 digits in a row" regex
* (e.g. "0912 345 6789", "0912-345-6789", "09123456789"), while NOT flagging ordinary
* numbers (prices, sizes, order codes, percentages) that aren't shaped like a phone
* number. The trick: find any run of digits *and common separators* long enough to
* matter, strip the separators back out, and only mask it if what's left is actually
* shaped like an Iranian phone number (mobile 09xxxxxxxxx, or landline 0[1-8]xxxxxxxx[x],
* optionally with a +98/0098/98 country-code prefix).
*/
const PERSIAN_DIGITS = '۰۱۲۳۴۵۶۷۸۹'
const ARABIC_DIGITS = '٠١٢٣٤٥٦٧٨٩'
function toWesternDigits (str) {
return str.replace(/[۰-۹٠-٩]/g, (ch) => {
const p = PERSIAN_DIGITS.indexOf(ch)
if (p !== -1) return String(p)
const a = ARABIC_DIGITS.indexOf(ch)
return a !== -1 ? String(a) : ch
})
}
/** Normalizes a leading country-code prefix (+98 / 0098 / 98) down to a plain leading 0. */
function normalizeLeadingCountryCode (digits) {
if (digits.startsWith('0098')) return `0${digits.slice(4)}`
if (digits.startsWith('98') && digits.length >= 12) return `0${digits.slice(2)}`
return digits
}
/**
* Iranian mobile: 09 + 9 digits = 11 digits.
* Iranian landline: 0 + area-code digit 1-8 (never 9, that's mobile) + 8-9 more digits,
* 10-11 digits total (Tehran's 021 + 8-digit local number is the 11-digit case; several
* other cities use a 3-digit area code + 7-digit local number, the 10-digit case).
*/
function isIranianPhoneNumber (rawDigits) {
const digits = normalizeLeadingCountryCode(rawDigits)
if (digits.length < 10 || digits.length > 11) return false
if (/^09\d{9}$/.test(digits)) return true
// کدِ شهرِ ۲ یا ۳ رقمی + شماره‌ی محلیِ ۷ یا ۸ رقمی = جمعاً ۱۰ یا ۱۱ رقم (با صفرِ ابتدایی)
if (/^0[1-8]\d{8,9}$/.test(digits)) return true
return false
}
// یک رقم، بعدش حداقل ۸ کاراکترِ رقم/جداکننده (فاصله، خط‌فاصله، نقطه، پرانتز، آندرلاین)،
// بعدش یک رقم — یعنی حداقل ۱۰ کاراکتر جمعاً، هم‌اندازه‌ی کوتاه‌ترین شماره‌ی معتبر
const CANDIDATE_RUN = /[0-9۰-۹٠-٩][0-9۰-۹٠-٩\s\-.()_+]{8,}[0-9۰-۹٠-٩]/g
/**
* Replaces any Iranian phone/mobile number found in `text` — including ones with
* digits spaced/dashed/dotted apart — with asterisks. Leaves everything else
* (prices, sizes, order numbers, short numbers) untouched.
*/
function maskPhoneNumbers (text) {
if (!text || typeof text !== 'string') return text
return text.replace(CANDIDATE_RUN, (match) => {
const digitsOnly = toWesternDigits(match).replace(/[^0-9]/g, '')
if (!isIranianPhoneNumber(digitsOnly)) return match
return '*'.repeat(match.length)
})
}
/** true فقط اگه واقعاً یه شماره‌ی ایرانی توی متن پیدا بشه — برای جاهایی که فقط لازمه‌ی چک‌کردن، نه ماسک‌کردن */
function containsPhoneNumber (text) {
if (!text || typeof text !== 'string') return false
let found = false
text.replace(CANDIDATE_RUN, (match) => {
const digitsOnly = toWesternDigits(match).replace(/[^0-9]/g, '')
if (isIranianPhoneNumber(digitsOnly)) found = true
return match
})
return found
}
module.exports = { maskPhoneNumbers, containsPhoneNumber }