This commit is contained in:
payacom
2026-08-31 20:45:41 +03:30
parent fa9f23c61d
commit 3f416a7a60
367 changed files with 621895 additions and 6443 deletions

View File

@@ -1,5 +1,5 @@
/* eslint-disable camelcase */
const { ProvinceModel, CityModel } = require('../../../models/StateCity')
const { CountryModel, ProvinceModel, CityModel } = require('../../../models/StateCity')
const UserModel = require('../../../models/UserModel')
const jwt = require('jsonwebtoken')
@@ -7,7 +7,7 @@ const setAddress = async (req, res, next) => {
try {
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
const { province_id, city_id, address, lat, lng, show_location } = req.body
const { country_id, province_id, city_id, address, lat, lng, show_location } = req.body
// اعتبارسنجی داده‌ها
if (
!province_id ||
@@ -37,9 +37,12 @@ const setAddress = async (req, res, next) => {
})
}
// یافتن استان و شهر
const province = await ProvinceModel.findOne({ id: province_id })
const city = await CityModel.findOne({ id: city_id })
// یافتن کشور، استان و شهر
const [country, province, city] = await Promise.all([
country_id ? CountryModel.findOne({ id: country_id }) : null,
ProvinceModel.findOne({ id: province_id }),
CityModel.findOne({ id: city_id })
])
if (!province || !city) {
return res.status(404).json({
error: true,
@@ -47,6 +50,9 @@ const setAddress = async (req, res, next) => {
})
}
if (country) {
user.country = { id: country.id, name: country.name_fa || country.name, iso2: country.iso2, slug: country.slug }
}
user.province = province
user.city = city
user.address = address
@@ -69,7 +75,7 @@ const updateAddress = async (req, res, next) => {
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
const { province_id, city_id, address, lat, lng, show_location } = req.body
const { country_id, province_id, city_id, address, lat, lng, show_location } = req.body
// اعتبارسنجی داده‌ها
if (
@@ -102,9 +108,12 @@ const updateAddress = async (req, res, next) => {
})
}
// یافتن استان و شهر با استفاده از شناسه استان و شهر
const province = await ProvinceModel.findOne({ id: province_id })
const city = await CityModel.findOne({ id: city_id })
// یافتن کشور، استان و شهر با استفاده از شناسه‌ها
const [country, province, city] = await Promise.all([
country_id ? CountryModel.findOne({ id: country_id }) : null,
ProvinceModel.findOne({ id: province_id }),
CityModel.findOne({ id: city_id })
])
if (!province || !city) {
return res.status(404).json({
error: true,
@@ -113,6 +122,9 @@ const updateAddress = async (req, res, next) => {
}
// به‌روزرسانی اطلاعات آدرس کاربر
if (country) {
user.country = { id: country.id, name: country.name_fa || country.name, iso2: country.iso2, slug: country.slug }
}
user.province = province
user.city = city
user.address = address

View File

@@ -2,14 +2,17 @@
const UserModel = require('../../../models/UserModel')
const { check, validationResult } = require('express-validator')
const jwt = require('jsonwebtoken')
const { isValidIranSheba, isValidIranNationalCode } = require('../../../utils/iranIdentity')
const saveAuthValidationRules = () => {
return [
check('shaba').notEmpty().withMessage('شماره شبا نمی‌تواند خالی باشد')
.matches(/^(?=.{24}$)[0-9]*$/).withMessage('فرمت شماره شبا صحیح نیست'),
.matches(/^(?=.{24}$)[0-9]*$/).withMessage('فرمت شماره شبا صحیح نیست')
.custom((value) => isValidIranSheba(value)).withMessage('شماره شبا معتبر نیست'),
check('birthday').notEmpty().withMessage('تاریخ تولد نمی‌تواند خالی باشد'),
check('national_code').notEmpty().withMessage('کد ملی نمی‌تواند خالی باشد')
.isLength({ min: 10, max: 10 }).withMessage('کد ملی باید 10 رقم باشد')
.custom((value) => isValidIranNationalCode(value)).withMessage('کد ملی معتبر نیست')
]
}

View File

@@ -1,5 +1,6 @@
const UserModel = require('../../../models/UserModel')
const jwt = require('jsonwebtoken')
const { syncVerifyBadge } = require('../../../utils/verifyBadge')
const completeRegistration = async (req, res, next) => {
try {
@@ -18,15 +19,33 @@ const completeRegistration = async (req, res, next) => {
user.is_Register = true
if (!user.is_verified || user.is_verified === 'none') {
user.is_verified = 'pending'
// سطح کاربر قبلاً فقط بعد از اولین کامنت امتیازدار (recalculateUserRating) ست
// می‌شد، پس کاربرهایی که هیچ‌وقت امتیاز نگرفته بودن سطح نداشتن (null). صریحاً
// اینجا هم ست می‌شه تا هر کاربر از لحظه‌ی تکمیل ثبت‌نام سطح واقعی داشته باشه
if (!user.user_level) {
user.user_level = 'تازه وارد'
}
// تیک خاکستری فقط وقتی کارت ملی ارسال شده باشد
const hasNationalCard = Boolean(
user.national_card_image && String(user.national_card_image).trim()
)
if (hasNationalCard) {
if (!user.is_verified || user.is_verified === 'none') {
user.is_verified = 'pending'
}
} else if (user.is_verified === 'pending') {
user.is_verified = 'none'
}
syncVerifyBadge(user)
await user.save()
return res.json({
message: 'ثبت نام تکمیل شد',
is_verified: user.is_verified,
verify_badge: user.verify_badge,
is_Register: user.is_Register
})
} catch (error) {

View File

@@ -2,11 +2,30 @@
const UserModel = require('../../../models/UserModel')
const jwt = require('jsonwebtoken')
function normalizeSubLabel(value) {
return String(value || '').trim()
}
function resolveDisplaySubExpertise(subList, displayValue) {
const normalizedSub = (Array.isArray(subList) ? subList : [])
.map(normalizeSubLabel)
.filter(Boolean)
if (!normalizedSub.length) return null
const display = normalizeSubLabel(displayValue)
if (display) {
const match = normalizedSub.find((item) => item === display)
if (match) return match
}
return normalizedSub[0]
}
const setExpertise = async (req, res, next) => {
try {
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
const { expertise, sub_expertise } = req.body
const { expertise, sub_expertise, display_sub_expertise } = req.body
if (!expertise || !sub_expertise) {
return res.status(422).json({
error: true,
@@ -14,6 +33,21 @@ const setExpertise = async (req, res, next) => {
})
}
const normalizedSub = Array.isArray(sub_expertise)
? sub_expertise.map(normalizeSubLabel).filter(Boolean)
: []
if (!normalizedSub.length) {
return res.status(422).json({
error: true,
message: 'حداقل یک زیرتخصص را انتخاب کنید'
})
}
const displayItem = resolveDisplaySubExpertise(
normalizedSub,
display_sub_expertise
)
// جستجوی کاربر با شماره موبایل ارسالی
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
const user = await UserModel.findById(decodedToken.id)
@@ -26,11 +60,15 @@ const setExpertise = async (req, res, next) => {
// آپدیت نام کاربری به صورت lowercase
user.expertise = expertise
user.sub_expertise = sub_expertise
user.sub_expertise = normalizedSub
user.display_sub_expertise = displayItem
await user.save()
return res.json({
message: 'تخصص با موفقیت به‌روزرسانی شد'
message: 'تخصص با موفقیت به‌روزرسانی شد',
expertise: user.expertise,
sub_expertise: user.sub_expertise,
display_sub_expertise: user.display_sub_expertise,
})
} catch (error) {
next(error)
@@ -42,7 +80,7 @@ const updateExpertise = async (req, res, next) => {
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
const { expertise, sub_expertise } = req.body
const { expertise, sub_expertise, display_sub_expertise } = req.body
// اعتبارسنجی داده‌ها
if (!expertise || !sub_expertise) {
@@ -52,6 +90,21 @@ const updateExpertise = async (req, res, next) => {
})
}
const normalizedSub = Array.isArray(sub_expertise)
? sub_expertise.map(normalizeSubLabel).filter(Boolean)
: []
if (!normalizedSub.length) {
return res.status(422).json({
error: true,
message: 'حداقل یک زیرتخصص را انتخاب کنید'
})
}
const displayItem = resolveDisplaySubExpertise(
normalizedSub,
display_sub_expertise
)
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
const userId = decodedToken.id
@@ -66,11 +119,15 @@ const updateExpertise = async (req, res, next) => {
// به‌روزرسانی اطلاعات تخصص و زیرتخصص
user.expertise = expertise
user.sub_expertise = sub_expertise
user.sub_expertise = normalizedSub
user.display_sub_expertise = displayItem
await user.save()
return res.json({
message: 'تخصص با موفقیت به‌روزرسانی شد'
message: 'تخصص با موفقیت به‌روزرسانی شد',
expertise: user.expertise,
sub_expertise: user.sub_expertise,
display_sub_expertise: user.display_sub_expertise,
})
} catch (error) {
next(error)

View File

@@ -0,0 +1,137 @@
/* eslint-disable camelcase */
const UserModel = require('../../../models/UserModel')
const jwt = require('jsonwebtoken')
const MARITAL_STATUS_VALUES = ['single', 'divorced']
const RESIDENCE_TYPE_VALUES = ['owner', 'tenant']
const VEHICLE_TYPE_VALUES = ['none', 'car', 'motorcycle']
const PERSONAL_STYLE_VALUES = ['formal', 'sporty']
const INCOME_RANGE_VALUES = ['under_5m', '5m_10m', '10m_20m', 'over_20m']
/** All fields are optional profile-enrichment fields — only the ones present
* in the request body are validated/applied, unlike sizes/cooperation-type
* where the whole group is required together. */
function applyPersonalDetails (user, body) {
if (body.marital_status !== undefined) {
if (body.marital_status !== null && !MARITAL_STATUS_VALUES.includes(body.marital_status)) {
return 'وضعیت تاهل نامعتبر است'
}
user.marital_status = body.marital_status
}
if (body.residence_type !== undefined) {
if (body.residence_type !== null && !RESIDENCE_TYPE_VALUES.includes(body.residence_type)) {
return 'نوع سکونت نامعتبر است'
}
user.residence_type = body.residence_type
}
if (body.residence_area !== undefined) {
user.residence_area = body.residence_area
}
if (body.vehicle_type !== undefined) {
if (body.vehicle_type !== null && !VEHICLE_TYPE_VALUES.includes(body.vehicle_type)) {
return 'نوع وسیله نقلیه نامعتبر است'
}
user.vehicle_type = body.vehicle_type
if (body.vehicle_type === 'none' || body.vehicle_type === null) {
user.vehicle_details = null
}
}
if (body.vehicle_details !== undefined && user.vehicle_type && user.vehicle_type !== 'none') {
user.vehicle_details = body.vehicle_details
}
if (body.personal_style !== undefined) {
if (body.personal_style !== null && !PERSONAL_STYLE_VALUES.includes(body.personal_style)) {
return 'استایل نامعتبر است'
}
user.personal_style = body.personal_style
}
if (body.job !== undefined) {
user.job = body.job
}
if (body.income_range !== undefined) {
if (body.income_range !== null && !INCOME_RANGE_VALUES.includes(body.income_range)) {
return 'بازه درآمد نامعتبر است'
}
user.income_range = body.income_range
}
if (body.moral_traits !== undefined) {
user.moral_traits = body.moral_traits
}
if (body.personal_details_public !== undefined) {
user.personal_details_public = Boolean(body.personal_details_public)
}
return null
}
const setPersonalDetails = async (req, res, next) => {
try {
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
const user = await UserModel.findById(decodedToken.id)
if (!user) {
return res.status(404).json({
error: true,
message: 'کاربری یافت نشد'
})
}
const validationError = applyPersonalDetails(user, req.body)
if (validationError) {
return res.status(422).json({ error: true, message: validationError })
}
await user.save()
return res.json({
message: 'مشخصات فردی با موفقیت ذخیره شد'
})
} catch (error) {
next(error)
}
}
const updatePersonalDetails = async (req, res, next) => {
try {
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
const user = await UserModel.findById(decodedToken.id)
if (!user) {
return res.status(404).json({
error: true,
message: 'کاربری یافت نشد'
})
}
const validationError = applyPersonalDetails(user, req.body)
if (validationError) {
return res.status(422).json({ error: true, message: validationError })
}
await user.save()
return res.json({
message: 'مشخصات فردی با موفقیت به‌روزرسانی شد',
marital_status: user.marital_status,
residence_type: user.residence_type,
residence_area: user.residence_area,
vehicle_type: user.vehicle_type,
vehicle_details: user.vehicle_details,
personal_style: user.personal_style,
job: user.job,
income_range: user.income_range,
moral_traits: user.moral_traits,
personal_details_public: user.personal_details_public === true
})
} catch (error) {
next(error)
}
}
module.exports = {
setPersonalDetails,
updatePersonalDetails
}

View File

@@ -8,14 +8,7 @@ const setProfileImage = async (req, res, next) => {
try {
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
const { mobile } = req.body
const { profile_image } = req.files
if (!mobile) {
return res.status(422).json({
error: true,
message: 'اطلاعات ارسالی اشتباه است'
})
}
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
const user = await UserModel.findById(decodedToken.id)
if (!user) {