From a7e4f684956619b86815f117f4bf1a89716e0c52 Mon Sep 17 00:00:00 2001 From: payacom <59262811+payacom@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:49:09 +0330 Subject: [PATCH] Initial commit --- .../register/usernameController.js | 63 +++++++++++++---- utils/usernameSuggestions.js | 69 +++++++++++++++++++ utils/usernameValidation.js | 33 +++++++++ 3 files changed, 150 insertions(+), 15 deletions(-) create mode 100644 utils/usernameSuggestions.js create mode 100644 utils/usernameValidation.js diff --git a/controllers/application/register/usernameController.js b/controllers/application/register/usernameController.js index 8b2428c..28752c4 100644 --- a/controllers/application/register/usernameController.js +++ b/controllers/application/register/usernameController.js @@ -1,6 +1,8 @@ /* eslint-disable camelcase */ const UserModel = require('../../../models/UserModel') const jwt = require('jsonwebtoken') +const { validateUsername } = require('../../../utils/usernameValidation') +const { generateUsernameSuggestions } = require('../../../utils/usernameSuggestions') const setUserName = async (req, res, next) => { try { @@ -20,32 +22,33 @@ const setUserName = async (req, res, next) => { message: 'کاربری با این شماره موبایل یافت نشد' }) } - // بررسی حداقل طول نام کاربری - if (user_name.length < 5) { + const validationError = validateUsername(user_name) + if (validationError) { return res.status(422).json({ error: true, - message: 'نام کاربری باید حداقل 5 کاراکتر باشد' + message: validationError }) } - // بررسی محدودیت‌های مجاز در نام کاربری - // const usernameRegex = /^[a-zA-Z0-9_]+$/ - // if (!usernameRegex.test(user_name)) { - // return res.status(422).json({ - // error: true, - // message: 'نام کاربری فقط می‌تواند شامل حروف الفبای انگلیسی، اعداد و کاراکتر _ باشد' - // }) - // } + + const normalizedUserName = user_name.trim().toLowerCase() + // بررسی تکراری بودن user_name - const existingUser = await UserModel.findOne({ user_name: { $regex: new RegExp('^' + user_name + '$', 'i') } }) + const existingUser = await UserModel.findOne({ user_name: { $regex: new RegExp('^' + normalizedUserName + '$', 'i') } }) if (existingUser && existingUser._id.toString() !== user._id.toString()) { + const suggestions = await generateUsernameSuggestions( + UserModel, + normalizedUserName, + user._id.toString() + ) return res.status(422).json({ error: true, - message: 'نام کاربری تکراری است' + message: 'نام کاربری تکراری است', + suggestions }) } // آپدیت نام کاربری به صورت lowercase - user.user_name = user_name.toLowerCase() + user.user_name = normalizedUserName await user.save() return res.json({ @@ -73,7 +76,37 @@ const updateUserName = async (req, res, next) => { }) } - const user = await UserModel.findByIdAndUpdate(userId, { user_name }, { new: true }) + const validationError = validateUsername(user_name) + if (validationError) { + return res.status(422).json({ + error: true, + message: validationError + }) + } + + const normalizedUserName = user_name.trim().toLowerCase() + + const existingUser = await UserModel.findOne({ + user_name: { $regex: new RegExp('^' + normalizedUserName + '$', 'i') } + }) + if (existingUser && existingUser._id.toString() !== userId) { + const suggestions = await generateUsernameSuggestions( + UserModel, + normalizedUserName, + userId + ) + return res.status(422).json({ + error: true, + message: 'نام کاربری تکراری است', + suggestions + }) + } + + const user = await UserModel.findByIdAndUpdate( + userId, + { user_name: normalizedUserName }, + { new: true } + ) if (!user) { return res.status(404).json({ diff --git a/utils/usernameSuggestions.js b/utils/usernameSuggestions.js new file mode 100644 index 0000000..315dcaa --- /dev/null +++ b/utils/usernameSuggestions.js @@ -0,0 +1,69 @@ +const { validateUsername, USERNAME_MAX_LENGTH } = require('./usernameValidation') + +const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + +const isUsernameTaken = async (UserModel, userName, excludeUserId) => { + const existing = await UserModel.findOne({ + user_name: { $regex: new RegExp(`^${escapeRegex(userName)}$`, 'i') } + }) + + if (!existing) return false + if (excludeUserId && existing._id.toString() === excludeUserId) return false + return true +} + +const buildCandidates = (base, attempt) => { + const random2 = () => String(Math.floor(Math.random() * 90 + 10)) + const random3 = () => String(Math.floor(Math.random() * 900 + 100)) + const random4 = () => String(Math.floor(Math.random() * 9000 + 1000)) + + const builders = [ + (b) => `${b}${random2()}`, + (b) => `${b}_${random3()}`, + (b) => `${b}.${random2()}`, + (b) => `${b}-${random2()}`, + (b) => `${b}${random4()}`, + (b) => `${b}_m${random2()}`, + (b) => `${b}${attempt}`, + ] + + return builders.map((fn) => { + let candidate = fn(base) + if (candidate.length > USERNAME_MAX_LENGTH) { + candidate = candidate.slice(0, USERNAME_MAX_LENGTH) + } + return candidate + }) +} + +const generateUsernameSuggestions = async ( + UserModel, + baseUsername, + excludeUserId, + count = 3 +) => { + const base = baseUsername.trim().toLowerCase() + const suggestions = [] + const tried = new Set([base]) + + for (let attempt = 1; attempt <= 60 && suggestions.length < count; attempt++) { + const candidates = buildCandidates(base, attempt) + + for (const candidate of candidates) { + if (suggestions.length >= count) break + if (tried.has(candidate)) continue + tried.add(candidate) + if (validateUsername(candidate)) continue + + const taken = await isUsernameTaken(UserModel, candidate, excludeUserId) + if (!taken) suggestions.push(candidate) + } + } + + return suggestions +} + +module.exports = { + generateUsernameSuggestions, + isUsernameTaken +} diff --git a/utils/usernameValidation.js b/utils/usernameValidation.js new file mode 100644 index 0000000..be99b38 --- /dev/null +++ b/utils/usernameValidation.js @@ -0,0 +1,33 @@ +/** حروف انگلیسی، اعداد و . _ - — بدون فارسی و کاراکترهای غیرمعمول */ +const USERNAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]{5,99}$/ + +const USERNAME_MIN_LENGTH = 6 +const USERNAME_MAX_LENGTH = 100 + +const validateUsername = (user_name) => { + if (!user_name || typeof user_name !== 'string') { + return 'نام کاربری الزامی است' + } + + const trimmed = user_name.trim() + if (trimmed.length < USERNAME_MIN_LENGTH) { + return `نام کاربری باید حداقل ${USERNAME_MIN_LENGTH} کاراکتر باشد` + } + + if (trimmed.length > USERNAME_MAX_LENGTH) { + return `نام کاربری نمی‌تواند بیشتر از ${USERNAME_MAX_LENGTH} کاراکتر باشد` + } + + if (!USERNAME_REGEX.test(trimmed)) { + return 'نام کاربری فقط می‌تواند شامل حروف انگلیسی، اعداد و کاراکترهای . _ - باشد' + } + + return null +} + +module.exports = { + USERNAME_REGEX, + USERNAME_MIN_LENGTH, + USERNAME_MAX_LENGTH, + validateUsername +}