Compare commits

..

3 Commits

Author SHA1 Message Date
payacom
2c6d8a4434 Initial commit 2026-07-07 13:23:55 +03:30
payacom
a7e4f68495 Initial commit 2026-07-07 12:49:09 +03:30
payacom
5719818b9d Initial commit 2026-07-07 12:29:12 +03:30
11 changed files with 231 additions and 28 deletions

View File

@@ -1,6 +1,7 @@
const UserModel = require('../../../models/UserModel')
const { check, validationResult } = require('express-validator')
const { default: axios } = require('axios')
const { OTP_VALID_MS } = require('../../../utils/otpExpiry')
const loginValidationRules = () => {
return [
check('mobile')
@@ -34,18 +35,22 @@ const loginUser = async (req, res, next) => {
}
const otp = generateOTP()
const otpSentAt = new Date()
// eslint-disable-next-line no-unused-vars
const user = await UserModel.findOneAndUpdate(
{ mobile },
{ $set: { mobile, otp } },
{ $set: { mobile, otp: String(otp), otpSentAt } },
{ upsert: true, new: true, lean: true }
)
// زمانبندی تنظیم مقدار otp به null پس از 10 دقیقه
setTimeout(() => {
UserModel.findOneAndUpdate({ mobile }, { $set: { otp: null } }, { new: true })
UserModel.findOneAndUpdate(
{ mobile },
{ $set: { otp: null, otpSentAt: null } },
{ new: true }
)
.then(() => {})
.catch(error => console.error('Error setting OTP to null:', error))
}, 5 * 60 * 1000)
}, OTP_VALID_MS)
const data = JSON.stringify({
mobile,
templateId: '930719',

View File

@@ -1,5 +1,6 @@
const UserModel = require('../../../models/UserModel')
const TokenService = require('../../../services/TokenService')
const { isOtpExpired } = require('../../../utils/otpExpiry')
// const jwt = require('jsonwebtoken')
const verifyUser = async (req, res, next) => {
@@ -20,7 +21,15 @@ const verifyUser = async (req, res, next) => {
})
}
const token = TokenService.sign({ id: user._id })
if (user.otp === otp) {
if (!user.otp || isOtpExpired(user)) {
return res.status(422).json({
error: true,
message: 'کد تایید منقضی شده است. لطفاً دوباره درخواست ارسال کد دهید'
})
}
if (String(user.otp) === String(otp)) {
// if (user.user_name === null) {
// return res.json({
// message: 'کد تایید صحیح بود',

View File

@@ -1,6 +1,7 @@
const { default: axios } = require('axios')
const UserModel = require('../../../models/UserModel')
const { check, validationResult } = require('express-validator')
const { OTP_VALID_MS } = require('../../../utils/otpExpiry')
// const { Token, VerificationCode } = require('sms-ir')
const registerValidationRules = () => {
@@ -36,17 +37,22 @@ const registerUser = async (req, res, next) => {
}
const otp = generateOTP()
const otpSentAt = new Date()
// eslint-disable-next-line no-unused-vars
const user = await UserModel.findOneAndUpdate(
{ mobile },
{ $set: { mobile, otp } },
{ $set: { mobile, otp: String(otp), otpSentAt } },
{ upsert: true, new: true, lean: true }
)
setTimeout(() => {
UserModel.findOneAndUpdate({ mobile }, { $set: { otp: null } }, { new: true })
UserModel.findOneAndUpdate(
{ mobile },
{ $set: { otp: null, otpSentAt: null } },
{ new: true }
)
.then(() => {})
.catch(error => console.error('Error setting OTP to null:', error))
}, 5 * 60 * 1000)
}, OTP_VALID_MS)
const data = JSON.stringify({
mobile,
templateId: '930719',

View File

@@ -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({

View File

@@ -1,5 +1,6 @@
const UserModel = require('../../../models/UserModel')
const TokenService = require('../../../services/TokenService')
const { isOtpExpired } = require('../../../utils/otpExpiry')
// const jwt = require('jsonwebtoken')
const verifyUser = async (req, res, next) => {
@@ -25,7 +26,14 @@ const verifyUser = async (req, res, next) => {
(!user.password) { step = 'password' } else if
(!user.first_name) { step = 'first_name' } else if
(!user.user_type) { step = 'user_type' } else { step = 'profile_image' }
if (user.otp === otp) {
if (!user.otp || isOtpExpired(user)) {
return res.status(422).json({
error: true,
message: 'کد تایید منقضی شده است. لطفاً دوباره درخواست ارسال کد دهید'
})
}
if (String(user.otp) === String(otp)) {
return res.json({
message: 'کد تایید صحیح بود',
token,

View File

@@ -33,7 +33,7 @@ const setProfileImage = async (req, res, next) => {
}
// ذخیره فایل عکس پروفایل
const uploadDir = path.join(__dirname, '../../../../storage/profiles')
const uploadDir = path.join(__dirname, '../../../storage/profiles')
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true })
}
@@ -78,7 +78,7 @@ const updateProfileImage = async (req, res, next) => {
})
}
const uploadDir = path.join(__dirname, '../../../../storage/profiles')
const uploadDir = path.join(__dirname, '../../../storage/profiles')
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true })

View File

@@ -98,14 +98,22 @@ const io = require('socket.io')(http, {
app.use(express.json({ limit: '1000mb' }));
app.use(express.urlencoded({ limit: '1000mb', extended: true }));
app.use('/storage', express.static(path.join(__dirname, 'storage')));
// مسیرهای مشخص قبل از /storage عمومی — پروفایل از هر دو محل (قدیم و جدید) سرو می‌شود
app.use('/storage/profiles', express.static(path.join(__dirname, '../storage/profiles')));
app.use('/storage/profiles', express.static(path.join(__dirname, 'storage/profiles')));
app.use('/storage/carts', express.static(path.join(__dirname, '../storage/carts')));
app.use('/storage/carts', express.static(path.join(__dirname, 'storage/carts')));
app.use('/storage/posts', express.static(path.join(__dirname, 'storage/posts')));
app.use('/storage/posts', express.static(path.join(__dirname, '../storage/posts')));
app.use('/storage/messages', express.static(path.join(__dirname, '../storage/messages')));
app.use('/storage/messages', express.static(path.join(__dirname, 'storage/messages')));
app.use('/storage/tickets', express.static(path.join(__dirname, '../storage/tickets')));
app.use('/storage/tickets', express.static(path.join(__dirname, 'storage/tickets')));
app.use('/storage/advertising', express.static(path.join(__dirname, '../storage/advertising')));
app.use('/storage/advertising', express.static(path.join(__dirname, 'storage/advertising')));
app.use('/storage/services', express.static(path.join(__dirname, '../storage/services')));
app.use('/storage/services', express.static(path.join(__dirname, 'storage/services')));
app.use('/storage', express.static(path.join(__dirname, 'storage')));
require('./boot');

View File

@@ -16,6 +16,11 @@ const userSchema = new mongoose.Schema({
maxLength: 6,
default: null
},
otpSentAt: {
type: Date,
required: false,
default: null
},
user_name: {
type: String,
required: false,

View File

@@ -1,4 +1,4 @@
const OTP_VALID_MS = 5 * 60 * 1000
const OTP_VALID_MS = 3 * 60 * 1000
const isOtpExpired = (user) => {
if (!user?.otp) return true

View File

@@ -0,0 +1,96 @@
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 splitBaseAndSuffix = (value) => {
const match = value.match(/^(.*?)(\d+)$/)
if (!match || !match[1]) {
return { core: value, suffix: null }
}
return { core: match[1], suffix: Number.parseInt(match[2], 10) }
}
const trimToMax = (value) =>
value.length > USERNAME_MAX_LENGTH ? value.slice(0, USERNAME_MAX_LENGTH) : value
/** پیشنهادهای مشابه: amiri2 → amiri3, amiri5, amiiri3, amiri334 */
const buildCandidates = (base) => {
const { core, suffix } = splitBaseAndSuffix(base)
const candidates = []
if (suffix !== null && !Number.isNaN(suffix)) {
candidates.push(`${core}${suffix + 1}`)
candidates.push(`${core}${suffix + 3}`)
candidates.push(`${core}${suffix}${Math.floor(Math.random() * 9 + 1)}`)
}
const numericSuffixes = [3, 5, 7, 12, 21, 34, 99, 334, 567]
numericSuffixes.forEach((num) => {
candidates.push(`${core}${num}`)
})
for (let i = 1; i < Math.min(core.length, 5); i += 1) {
const ch = core[i]
if (!ch) continue
const doubled = `${core.slice(0, i)}${ch}${core.slice(i)}`
candidates.push(`${doubled}${suffix !== null ? suffix + 1 : 3}`)
candidates.push(`${doubled}${Math.floor(Math.random() * 90 + 10)}`)
}
candidates.push(`${core}_${suffix !== null ? suffix + 1 : 1}`)
candidates.push(`${core}.${Math.floor(Math.random() * 900 + 100)}`)
candidates.push(`${core}-${Math.floor(Math.random() * 90 + 10)}`)
return [...new Set(candidates.map(trimToMax))]
}
const generateUsernameSuggestions = async (
UserModel,
baseUsername,
excludeUserId,
count = 3
) => {
const base = baseUsername.trim().toLowerCase()
const suggestions = []
const tried = new Set([base])
for (let attempt = 0; attempt < 80 && suggestions.length < count; attempt += 1) {
const candidates = buildCandidates(base)
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)
}
if (suggestions.length < count) {
const fallback = trimToMax(`${base}${Math.floor(Math.random() * 9000 + 1000)}`)
if (!tried.has(fallback) && !validateUsername(fallback)) {
tried.add(fallback)
const taken = await isUsernameTaken(UserModel, fallback, excludeUserId)
if (!taken) suggestions.push(fallback)
}
}
}
return suggestions
}
module.exports = {
generateUsernameSuggestions,
isUsernameTaken
}

View File

@@ -0,0 +1,33 @@
/** حروف انگلیسی، اعداد و . _ - — بدون فارسی و کاراکترهای غیرمعمول */
const USERNAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]{7,99}$/
const USERNAME_MIN_LENGTH = 8
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
}