feat(professors,sms): merge professor with users, add existing user link, professor portal endpoint, and SMS bypass list

This commit is contained in:
2026-08-25 03:48:19 +03:30
parent b708a6b777
commit 0a022c0917
14 changed files with 865 additions and 205 deletions
+53 -5
View File
@@ -13,17 +13,32 @@ let cache = null;
let cacheAt = 0;
const CACHE_TTL_MS = 10000;
const normalizePhoneForBypass = (raw = '') => {
if (!raw) return '';
let digits = String(raw)
.replace(/[۰-۹]/g, (d) => '0123456789'['۰۱۲۳۴۵۶۷۸۹'.indexOf(d)])
.replace(/[٠-٩]/g, (d) => '0123456789'['٠١٢٣٤٥٦٧٨٩'.indexOf(d)])
.replace(/\D/g, '');
if (digits.startsWith('98') && digits.length >= 12) digits = `0${digits.slice(2)}`;
if (digits.length === 10 && digits.startsWith('9')) digits = `0${digits}`;
if (digits.length > 11 && digits.startsWith('09')) digits = digits.slice(0, 11);
return digits;
};
const readDbFlags = (doc) => ({
smsEnabled: doc?.smsEnabled,
emailEnabled: doc?.emailEnabled,
botEnabled: doc?.botEnabled
botEnabled: doc?.botEnabled,
smsBypassNumbers: Array.isArray(doc?.smsBypassNumbers)
? doc.smsBypassNumbers.map((item) => (item.toObject ? item.toObject() : item))
: []
});
const getDbMessagingFlags = async () => {
const now = Date.now();
if (cache && (now - cacheAt) < CACHE_TTL_MS) return cache;
const doc = await Setting.findOne({ key: SETTINGS_KEY })
.select('smsEnabled emailEnabled botEnabled')
.select('smsEnabled emailEnabled botEnabled smsBypassNumbers')
.lean();
cache = readDbFlags(doc);
cacheAt = now;
@@ -35,13 +50,42 @@ const invalidateMessagingCache = () => {
cacheAt = 0;
};
const getMessagingChannelState = async (channel) => {
const isSmsBypassNumber = async (phoneNumber) => {
if (!phoneNumber) return false;
const target = normalizePhoneForBypass(phoneNumber);
if (!target) return false;
try {
const db = await getDbMessagingFlags();
const bypassList = Array.isArray(db.smsBypassNumbers) ? db.smsBypassNumbers : [];
return bypassList.some((item) => item.isActive !== false && normalizePhoneForBypass(item.phoneNumber) === target);
} catch (err) {
logger.warn(`[Messaging] Failed to check bypass numbers: ${err.message}`);
return false;
}
};
const getMessagingChannelState = async (channel, recipientPhone = null) => {
const envEnabled = isEnvChannelEnabled(channel, config);
const bypass = channel === 'sms' && recipientPhone ? await isSmsBypassNumber(recipientPhone) : false;
if (bypass) {
logger.info(`[Messaging] Recipient ${recipientPhone} is in SMS bypass list. Channel bypass activated.`);
return {
enabled: true,
envEnabled,
dbEnabled: true,
bypass: true,
blockReason: null
};
}
if (!envEnabled) {
return {
enabled: false,
envEnabled: false,
dbEnabled: null,
bypass: false,
blockReason: 'env_disabled'
};
}
@@ -53,6 +97,7 @@ const getMessagingChannelState = async (channel) => {
enabled: dbEnabled,
envEnabled: true,
dbEnabled,
bypass: false,
blockReason: dbEnabled ? null : 'dashboard_disabled'
};
} catch (err) {
@@ -61,13 +106,14 @@ const getMessagingChannelState = async (channel) => {
enabled: true,
envEnabled: true,
dbEnabled: null,
bypass: false,
blockReason: null
};
}
};
const isMessagingChannelEnabled = async (channel) => {
const state = await getMessagingChannelState(channel);
const isMessagingChannelEnabled = async (channel, recipientPhone = null) => {
const state = await getMessagingChannelState(channel, recipientPhone);
return state.enabled;
};
@@ -76,6 +122,8 @@ const getPublicMessaging = (doc) => toPublicMessaging(readDbFlags(doc), config);
module.exports = {
getMessagingChannelState,
isMessagingChannelEnabled,
isSmsBypassNumber,
normalizePhoneForBypass,
invalidateMessagingCache,
getPublicMessaging,
getDbMessagingFlags