This commit is contained in:
payacom
2026-10-01 21:12:05 +03:30
parent 806471241e
commit d4ebc20aa8
41 changed files with 1015 additions and 135 deletions

View File

@@ -1,5 +1,5 @@
const mongoose = require('mongoose')
const crypto = require('crypto')
const bcrypt = require('bcryptjs')
const UserModel = require('../../../models/UserModel')
const DeviceLinkSessionModel = require('../../../models/DeviceLinkSessionModel')
@@ -154,34 +154,26 @@ const pollDeviceLink = async (req, res, next) => {
}
/**
* رمزِ خودکارِ per-account برای رمزنگاری/بازگشاییِ خودکارِ بک‌آپِ کلیدِ E2EE — به‌جای
* PIN دستیِ کاربر. با یه درخواستِ احرازهویت‌شده به همین اکانت (از هر دستگاهی) قابل‌
* دریافته، پس بازیابیِ هویت روی دستگاهِ جدید بدون هیچ دخالتِ کاربری انجام می‌شه.
* اولین‌بار که خواسته بشه lazily ساخته می‌شه؛ با یه آپدیتِ شرطی (فقط اگه هنوز null
* باشه) از race بینِ دو دستگاهی که هم‌زمان اولین‌بار درخواستش می‌کنن جلوگیری می‌شه.
* کلیدِ E2EE حالا از رمزِ عبورِ خودِ کاربر مشتق می‌شه (نه یه رمزِ خودکارِ ساخته‌شده‌ی
* سرور) — این اندپوینت فقط رمزِ واردشده رو با bcrypt در برابرِ هشِ ذخیره‌شده تایید
* می‌کنه (عینِ loginWithUserName.js)، تا کلاینت قبل از اجرای PBKDF2ِ ۲۵۰هزار-تکراریِ
* محلی (برای decryptPrivateKeyFromBackup) یه فیدبکِ سریع برای «رمزِ اشتباه» داشته
* باشه؛ خودِ مشتق‌سازیِ کلید کاملاً سمتِ کلاینت انجام می‌شه، سرور هیچ‌وقت کلید رو نمی‌بینه
*/
const getOrCreateAutoBackupSecret = async (req, res, next) => {
const verifyPassword = async (req, res, next) => {
try {
const userId = req.user._id
const existing = await UserModel.findById(userId).select('e2ee_auto_backup_secret').lean()
if (existing?.e2ee_auto_backup_secret) {
return res.json({ secret: existing.e2ee_auto_backup_secret })
const { password } = req.body
if (!password || typeof password !== 'string') {
return res.status(422).json({ error: 'password is required' })
}
const candidate = crypto.randomBytes(32).toString('base64')
const updated = await UserModel.findOneAndUpdate(
{ _id: userId, e2ee_auto_backup_secret: null },
{ $set: { e2ee_auto_backup_secret: candidate } },
{ new: true }
).select('e2ee_auto_backup_secret').lean()
const user = await UserModel.findById(req.user._id).select('password').lean()
if (!user?.password) {
return res.json({ valid: false })
}
// اگه updated خالی برگشت یعنی یه درخواستِ هم‌زمانِ دیگه (دستگاهِ دیگه) بین
// خوندنِ بالا و همین آپدیت، خودش رمز رو ساخته بود — بخونش تا هر دو دستگاه دقیقاً
// یه رمزِ یکسان بگیرن، نه دوتا رمزِ متفاوت
const secret = updated?.e2ee_auto_backup_secret ||
(await UserModel.findById(userId).select('e2ee_auto_backup_secret').lean())?.e2ee_auto_backup_secret
res.json({ secret })
const valid = await bcrypt.compare(password, user.password)
res.json({ valid })
} catch (error) {
next(error)
}
@@ -243,7 +235,7 @@ module.exports = {
getDeviceLinkSession,
approveDeviceLink,
pollDeviceLink,
getOrCreateAutoBackupSecret,
verifyPassword,
putKeyBackup,
getKeyBackup,
deleteKeyBackup

View File

@@ -0,0 +1,129 @@
const UserModel = require('../../../models/UserModel')
const { OTP_VALID_MS, isOtpExpired } = require('../../../utils/otpExpiry')
const { sendSmsIrVerify, getSmsIrTemplateId } = require('../../../utils/smsIr')
function normalizeMobile (value) {
return String(value || '').replace(/\D/g, '')
}
function generateOTP () {
return Math.floor(100000 + Math.random() * 900000)
}
// کاربرانی که با گوگل/اینستاگرام ثبت‌نام کردن و شماره موبایل ندارن، از همین‌جا
// (نه فلوی لاگین) می‌تونن یه بار برای همیشه شماره‌شون رو ثبت کنن. بعد از ثبت،
// دیگه هیچ مسیرِ ویرایشی براش نیست — طبق خواسته‌ی کاربر.
const requestMobileOtp = async (req, res, next) => {
try {
if (req.user.mobile) {
return res.status(422).json({
error: true,
message: 'شماره موبایل شما قبلاً ثبت شده و قابل تغییر نیست'
})
}
const mobile = normalizeMobile(req.body.mobile)
if (!mobile || !/^09[0-9]{9}$/.test(mobile)) {
return res.status(422).json({
error: true,
message: 'فرمت شماره موبایل صحیح نیست'
})
}
const existing = await UserModel.findOne({ mobile })
if (existing) {
return res.status(422).json({
error: true,
message: 'این شماره موبایل قبلاً توسط کاربر دیگری ثبت شده است'
})
}
const otp = generateOTP()
const otpSentAt = new Date()
await UserModel.updateOne(
{ _id: req.user._id },
{ $set: { pending_mobile: mobile, otp: String(otp), otpSentAt } }
)
setTimeout(() => {
UserModel.updateOne(
{ _id: req.user._id, otp: String(otp) },
{ $set: { otp: null, otpSentAt: null, pending_mobile: null } }
).catch((error) => console.error('[requestMobileOtp] clear OTP failed:', error))
}, OTP_VALID_MS)
const smsResult = await sendSmsIrVerify({
mobile,
otp,
templateId: getSmsIrTemplateId('930719')
})
if (!smsResult.ok) {
return res.status(502).json({
error: true,
message: 'ارسال پیامک ناموفق بود. لطفاً کمی بعد دوباره تلاش کنید.'
})
}
return res.json({ success: true, message: 'کد تایید ارسال شد' })
} catch (error) {
next(error)
}
}
const verifyMobileOtp = async (req, res, next) => {
try {
const { otp } = req.body
if (!otp) {
return res.status(422).json({ error: true, message: 'کد تایید الزامی است' })
}
const user = await UserModel.findById(req.user._id)
if (!user.pending_mobile || !user.otp) {
return res.status(422).json({
error: true,
message: 'درخواستی برای ثبت شماره موبایل یافت نشد. دوباره تلاش کنید'
})
}
if (isOtpExpired(user)) {
return res.status(422).json({
error: true,
message: 'کد تایید منقضی شده است. لطفاً دوباره درخواست ارسال کد دهید'
})
}
if (String(user.otp) !== String(otp)) {
return res.status(422).json({ error: true, message: 'کد را اشتباه وارد کردید' })
}
// بینِ درخواستِ کد و تاییدش، ممکنه یه کاربرِ دیگه دقیقاً همین شماره رو گرفته باشه
const conflict = await UserModel.findOne({ mobile: user.pending_mobile })
if (conflict) {
return res.status(422).json({
error: true,
message: 'این شماره موبایل قبلاً توسط کاربر دیگری ثبت شده است'
})
}
user.mobile = user.pending_mobile
user.pending_mobile = null
user.otp = null
user.otpSentAt = null
await user.save()
return res.json({
success: true,
message: 'شماره موبایل با موفقیت ثبت شد',
mobile: user.mobile
})
} catch (error) {
next(error)
}
}
module.exports = {
requestMobileOtp,
verifyMobileOtp
}