nodebb-plugin-phone-verification 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/library.js CHANGED
@@ -29,6 +29,9 @@ const defaultSettings = {
29
29
  voiceServerUrl: 'https://www.call2all.co.il/ym/api/RunTzintuk',
30
30
  voiceServerApiKey: '',
31
31
  voiceServerEnabled: false,
32
+ userCallEnabled: false,
33
+ userCallNumber: '',
34
+ callApiToken: '',
32
35
  blockUnverifiedUsers: false,
33
36
  // הוסרו הגדרות TTS שאינן רלוונטיות לצינתוק
34
37
  };
@@ -51,6 +54,20 @@ plugin.hashCode = function (code) {
51
54
  return crypto.createHash('sha256').update(code).digest('hex');
52
55
  };
53
56
 
57
+ plugin.generateApiToken = function () {
58
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
59
+ const bytes = crypto.randomBytes(16);
60
+ let token = '';
61
+ for (let i = 0; i < 16; i++) {
62
+ token += chars[bytes[i] % chars.length];
63
+ }
64
+ return token;
65
+ };
66
+
67
+ plugin.generateNumericCode = function () {
68
+ return String(Math.floor(1000 + Math.random() * 9000));
69
+ };
70
+
54
71
  // ==================== בדיקת הרשאות ====================
55
72
 
56
73
  plugin.checkPostingPermissions = async function (data) {
@@ -189,7 +206,7 @@ plugin.sendTzintuk = async function (phone) {
189
206
 
190
207
  // ==================== ניהול נתונים (Redis) ====================
191
208
 
192
- plugin.saveVerificationCode = async function (phone, code) {
209
+ plugin.saveVerificationCode = async function (phone, code, options = {}) {
193
210
  const normalizedPhone = plugin.normalizePhone(phone);
194
211
  const now = Date.now();
195
212
  const expiresAt = now + (CODE_EXPIRY_MINUTES * 60 * 1000);
@@ -203,6 +220,9 @@ plugin.saveVerificationCode = async function (phone, code) {
203
220
  }
204
221
 
205
222
  const data = { hashedCode: plugin.hashCode(code), attempts: 0, createdAt: now, expiresAt: expiresAt, blockedUntil: 0 };
223
+ if (options.storePlain === true) {
224
+ data.plainCode = String(code);
225
+ }
206
226
  await db.setObject(key, data);
207
227
  await db.pexpireAt(key, now + (20 * 60 * 1000));
208
228
  return { success: true, expiresAt };
@@ -277,6 +297,23 @@ plugin.clearVerifiedPhone = async function (phone) {
277
297
  if (db) await db.delete(`phone-verification:verified:${normalizedPhone}`);
278
298
  };
279
299
 
300
+ plugin.getPendingPlainCode = async function (phone) {
301
+ const normalizedPhone = plugin.normalizePhone(phone);
302
+ const now = Date.now();
303
+ const key = `${REDIS_PREFIX}${normalizedPhone}`;
304
+ if (!db) return { success: false, error: 'DB_ERROR' };
305
+ const data = await db.getObject(key);
306
+ if (!data) return { success: false, error: 'CODE_NOT_FOUND' };
307
+ if (data.blockedUntil && parseInt(data.blockedUntil, 10) > now) {
308
+ return { success: false, error: 'PHONE_BLOCKED' };
309
+ }
310
+ if (parseInt(data.expiresAt, 10) < now) {
311
+ return { success: false, error: 'CODE_EXPIRED' };
312
+ }
313
+ if (!data.plainCode) return { success: false, error: 'CODE_UNAVAILABLE' };
314
+ return { success: true, code: String(data.plainCode) };
315
+ };
316
+
280
317
  // ==================== DB Users Logic ====================
281
318
  plugin.savePhoneToUser = async function (uid, phone, verified = true, forceOverride = false) {
282
319
  if (!db || !User) return { success: false };
@@ -511,6 +548,9 @@ plugin.init = async function (params) {
511
548
  router.post('/api/phone-verification/verify-code', middleware.applyCSRF, plugin.apiVerifyCode);
512
549
  router.post('/api/phone-verification/initiate-call', middleware.applyCSRF, plugin.apiInitiateCall);
513
550
  router.post('/api/phone-verification/check-status', middleware.applyCSRF, plugin.apiCheckStatus);
551
+ router.post('/api/phone-verification/request-user-call', middleware.applyCSRF, plugin.apiRequestUserCall);
552
+ router.get('/api/phone-verification/inbound-call', plugin.apiInboundCall);
553
+ router.get('/api/phone-verification/public-settings', plugin.apiGetPublicSettings);
514
554
 
515
555
  // User Profile APIs
516
556
  router.get('/api/user/:userslug/phone', middleware.authenticateRequest, plugin.apiGetUserPhoneProfile);
@@ -527,6 +567,7 @@ plugin.init = async function (params) {
527
567
  router.get('/api/admin/plugins/phone-verification/settings', middleware.admin.checkPrivileges, plugin.apiAdminGetSettings);
528
568
  router.post('/api/admin/plugins/phone-verification/settings', middleware.admin.checkPrivileges, middleware.applyCSRF, plugin.apiAdminSaveSettings);
529
569
  router.post('/api/admin/plugins/phone-verification/test-call', middleware.admin.checkPrivileges, middleware.applyCSRF, plugin.apiAdminTestCall);
570
+ router.post('/api/admin/plugins/phone-verification/refresh-token', middleware.admin.checkPrivileges, middleware.applyCSRF, plugin.apiAdminRefreshToken);
530
571
  };
531
572
  plugin.apiCheckStatus = async function (req, res) {
532
573
  try {
@@ -540,10 +581,19 @@ plugin.getSettings = async function () {
540
581
 
541
582
  const isTrue = (val) => val === true || val === 'true' || val === 'on' || val === '1';
542
583
 
584
+ if (!settings.callApiToken) {
585
+ const newToken = plugin.generateApiToken();
586
+ await meta.settings.set('phone-verification', { ...settings, callApiToken: newToken });
587
+ settings.callApiToken = newToken;
588
+ }
589
+
543
590
  return {
544
591
  voiceServerUrl: settings.voiceServerUrl || defaultSettings.voiceServerUrl,
545
592
  voiceServerApiKey: settings.voiceServerApiKey || '',
546
593
  voiceServerEnabled: isTrue(settings.voiceServerEnabled),
594
+ userCallEnabled: isTrue(settings.userCallEnabled),
595
+ userCallNumber: settings.userCallNumber || '',
596
+ callApiToken: settings.callApiToken || '',
547
597
  blockUnverifiedUsers: isTrue(settings.blockUnverifiedUsers)
548
598
  };
549
599
  };
@@ -554,6 +604,9 @@ plugin.saveSettings = async function (settings) {
554
604
  voiceServerUrl: settings.voiceServerUrl || defaultSettings.voiceServerUrl,
555
605
  voiceServerApiKey: settings.voiceServerApiKey || '',
556
606
  voiceServerEnabled: settings.voiceServerEnabled ? 'true' : 'false',
607
+ userCallEnabled: settings.userCallEnabled ? 'true' : 'false',
608
+ userCallNumber: settings.userCallNumber || '',
609
+ callApiToken: settings.callApiToken || '',
557
610
  blockUnverifiedUsers: settings.blockUnverifiedUsers ? 'true' : 'false'
558
611
  });
559
612
  return true;
@@ -570,14 +623,24 @@ plugin.apiAdminGetSettings = async function (req, res) {
570
623
 
571
624
  plugin.apiAdminSaveSettings = async function (req, res) {
572
625
  try {
573
- const { voiceServerApiKey, ...rest } = req.body;
626
+ const { voiceServerApiKey, callApiToken, ...rest } = req.body;
574
627
  const current = await plugin.getSettings();
575
628
  const apiKey = voiceServerApiKey === '********' ? current.voiceServerApiKey : voiceServerApiKey;
576
- await plugin.saveSettings({ ...rest, voiceServerApiKey: apiKey });
629
+ const tokenToSave = callApiToken || current.callApiToken;
630
+ await plugin.saveSettings({ ...rest, voiceServerApiKey: apiKey, callApiToken: tokenToSave });
577
631
  res.json({ success: true });
578
632
  } catch (err) { res.json({ success: false }); }
579
633
  };
580
634
 
635
+ plugin.apiAdminRefreshToken = async function (req, res) {
636
+ try {
637
+ const current = await plugin.getSettings();
638
+ const newToken = plugin.generateApiToken();
639
+ await plugin.saveSettings({ ...current, callApiToken: newToken });
640
+ res.json({ success: true, token: newToken });
641
+ } catch (err) { res.json({ success: false }); }
642
+ };
643
+
581
644
  plugin.apiAdminTestCall = async function (req, res) {
582
645
  try {
583
646
  const { phoneNumber } = req.body;
@@ -624,6 +687,40 @@ plugin.apiSendCode = async function (req, res) {
624
687
  }
625
688
  };
626
689
 
690
+ plugin.apiRequestUserCall = async function (req, res) {
691
+ try {
692
+ const { phoneNumber } = req.body;
693
+ if (!phoneNumber) return res.json({ success: false, error: 'MISSING' });
694
+
695
+ const settings = await plugin.getSettings();
696
+ if (!settings.userCallEnabled) {
697
+ return res.json({ success: false, error: 'CALL_DISABLED', message: 'אימות בשיחה יזומה אינו פעיל' });
698
+ }
699
+
700
+ const clientIp = req.ip || req.headers['x-forwarded-for'];
701
+ const ipCheck = await plugin.checkIpRateLimit(clientIp);
702
+ if (!ipCheck.allowed) return res.json(ipCheck);
703
+ await plugin.incrementIpCounter(clientIp);
704
+
705
+ const clean = plugin.normalizePhone(phoneNumber.replace(/\D/g, ''));
706
+ if (!plugin.validatePhoneNumber(clean)) return res.json({ success: false, error: 'INVALID' });
707
+
708
+ const existingUid = await plugin.findUserByPhone(clean);
709
+ if (existingUid && (!req.uid || parseInt(existingUid) !== parseInt(req.uid))) {
710
+ return res.json({ success: false, error: 'EXISTS', message: 'המספר תפוס' });
711
+ }
712
+
713
+ const code = plugin.generateNumericCode();
714
+ const saveResult = await plugin.saveVerificationCode(clean, code, { storePlain: true });
715
+ if (!saveResult.success) return res.json(saveResult);
716
+
717
+ res.json({ success: true, message: 'הקוד הוכן. נא להתקשר לקו לצורך שמיעת הקוד.', callNumber: settings.userCallNumber || '' });
718
+ } catch (err) {
719
+ console.error(err);
720
+ res.json({ success: false });
721
+ }
722
+ };
723
+
627
724
  plugin.apiVerifyCode = async function (req, res) {
628
725
  try {
629
726
  const { phoneNumber, code } = req.body;
@@ -677,6 +774,49 @@ plugin.apiInitiateCall = async function (req, res) {
677
774
  }
678
775
  };
679
776
 
777
+ plugin.apiInboundCall = async function (req, res) {
778
+ try {
779
+ const settings = await plugin.getSettings();
780
+ const token = req.query.token;
781
+ const phone = req.query.ApiPhone;
782
+
783
+ if (!token || token !== settings.callApiToken) {
784
+ return res.status(403).send('forbidden');
785
+ }
786
+ if (!phone || !plugin.validatePhoneNumber(phone)) {
787
+ return res.status(400).send('invalid');
788
+ }
789
+
790
+ const pending = await plugin.getPendingPlainCode(phone);
791
+ if (!pending.success) {
792
+ return res.status(404).send('not found');
793
+ }
794
+
795
+ const code = pending.code;
796
+ const payload = `read=t-הקוד שלכם החד פעמי הוא.d-${code}.t-לשמיעה חוזרת הַקִּישׁוּ1=MOP,,1,1,15,NO,,,,1,3,OK,,,no`;
797
+ res.set('Content-Type', 'text/plain; charset=utf-8');
798
+ res.send(payload);
799
+ } catch (err) {
800
+ res.status(500).send('error');
801
+ }
802
+ };
803
+
804
+ plugin.apiGetPublicSettings = async function (req, res) {
805
+ try {
806
+ const settings = await plugin.getSettings();
807
+ res.json({
808
+ success: true,
809
+ settings: {
810
+ voiceServerEnabled: settings.voiceServerEnabled,
811
+ userCallEnabled: settings.userCallEnabled,
812
+ userCallNumber: settings.userCallNumber || ''
813
+ }
814
+ });
815
+ } catch (e) {
816
+ res.json({ success: false });
817
+ }
818
+ };
819
+
680
820
  plugin.apiGetUserPhoneProfile = async function (req, res) {
681
821
  try {
682
822
  const uid = await User.getUidByUserslug(req.params.userslug);
@@ -775,4 +915,4 @@ plugin.userDelete = async function (data) {
775
915
  } catch (e) {}
776
916
  };
777
917
 
778
- module.exports = plugin;
918
+ module.exports = plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-phone-verification",
3
- "version": "2.0.1",
3
+ "version": "3.0.0",
4
4
  "description": "אימות מספר טלפון נייד בתהליך ההרשמה לפורום NodeBB",
5
5
  "main": "library.js",
6
6
  "repository": {
package/plugin.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "id": "nodebb-plugin-phone-verification",
3
3
  "name": "Phone Verification",
4
4
  "description": "אימות מספר טלפון נייד בתהליך ההרשמה לפורום ובפרופיל המשתמש",
5
- "version": "2.0.1",
5
+ "version": "3.0.0",
6
6
  "library": "./library.js",
7
7
  "hooks": [
8
8
  { "hook": "filter:register.check", "method": "checkRegistration" },
@@ -11,6 +11,76 @@ define('admin/plugins/phone-verification', ['settings', 'bootbox', 'alerts'], fu
11
11
 
12
12
  Settings.load('phone-verification', $('#voice-settings-form'));
13
13
 
14
+ function buildApiLink() {
15
+ var base = window.location.origin || '';
16
+ return base + config.relative_path + '/api/phone-verification/inbound-call';
17
+ }
18
+
19
+ function buildUserCallConfig(token) {
20
+ var apiLink = buildApiLink();
21
+ return [
22
+ 'type=api',
23
+ 'api_link=' + apiLink,
24
+ 'api_hangup_send=no',
25
+ 'api_add_0=token=' + token
26
+ ].join('\n');
27
+ }
28
+
29
+ function showUserCallSetupModal(token, onConfirm) {
30
+ var configText = buildUserCallConfig(token);
31
+ var modalHtml =
32
+ '<div>' +
33
+ '<p>לשם ביצוע הפעולה יש להגדיר בקו שלכם בהגדרות השלוחה הראשית או השלוחה הרצויה לאימות את ההגדרות הבאות:</p>' +
34
+ '<div class="mb-2">' +
35
+ '<button type="button" class="btn btn-default btn-sm" id="copy-user-call-config">העתק</button>' +
36
+ '</div>' +
37
+ '<pre style="white-space: pre-wrap;"><code id="user-call-config">' + configText + '</code></pre>' +
38
+ '</div>';
39
+
40
+ var dialog = bootbox.dialog({
41
+ title: 'הגדרת שיחה יזומה',
42
+ message: modalHtml,
43
+ buttons: {
44
+ cancel: {
45
+ label: 'ביטול',
46
+ className: 'btn-ghost',
47
+ callback: function() {
48
+ if (typeof onConfirm === 'function') onConfirm(false);
49
+ }
50
+ },
51
+ ok: {
52
+ label: 'הגדרתי את הקו שיעביר להמשך',
53
+ className: 'btn-primary',
54
+ callback: function() {
55
+ if (typeof onConfirm === 'function') onConfirm(true);
56
+ }
57
+ }
58
+ }
59
+ });
60
+
61
+ dialog.on('shown.bs.modal', function() {
62
+ $('#copy-user-call-config').off('click').on('click', function() {
63
+ var text = $('#user-call-config').text();
64
+ if (navigator.clipboard && navigator.clipboard.writeText) {
65
+ navigator.clipboard.writeText(text).then(function() {
66
+ alerts.success('הועתק ללוח');
67
+ }).catch(function() {
68
+ alerts.error('לא ניתן להעתיק');
69
+ });
70
+ } else {
71
+ var $temp = $('<textarea>').val(text).appendTo('body').select();
72
+ try {
73
+ document.execCommand('copy');
74
+ alerts.success('הועתק ללוח');
75
+ } catch (e) {
76
+ alerts.error('לא ניתן להעתיק');
77
+ }
78
+ $temp.remove();
79
+ }
80
+ });
81
+ });
82
+ }
83
+
14
84
  $('#save-settings-btn').on('click', function(e) {
15
85
  e.preventDefault();
16
86
  Settings.save('phone-verification', $('#voice-settings-form'), function() {
@@ -18,6 +88,53 @@ define('admin/plugins/phone-verification', ['settings', 'bootbox', 'alerts'], fu
18
88
  });
19
89
  });
20
90
 
91
+ $('#userCallEnabled').on('change', function() {
92
+ var $checkbox = $(this);
93
+ if (!$checkbox.is(':checked')) return;
94
+
95
+ var token = $('#callApiToken').val();
96
+ if (!token) {
97
+ $.post(config.relative_path + '/api/admin/plugins/phone-verification/refresh-token', { _csrf: config.csrf_token }, function(res) {
98
+ if (res && res.success && res.token) {
99
+ $('#callApiToken').val(res.token);
100
+ showUserCallSetupModal(res.token, function(confirmed) {
101
+ if (!confirmed) {
102
+ $checkbox.prop('checked', false);
103
+ }
104
+ });
105
+ } else {
106
+ alerts.error('שגיאה ביצירת טוקן');
107
+ $checkbox.prop('checked', false);
108
+ }
109
+ });
110
+ return;
111
+ }
112
+
113
+ showUserCallSetupModal(token, function(confirmed) {
114
+ if (!confirmed) {
115
+ $checkbox.prop('checked', false);
116
+ }
117
+ });
118
+ });
119
+
120
+ $('#refresh-call-token-btn').on('click', function() {
121
+ bootbox.confirm({
122
+ title: 'רענון טוקן',
123
+ message: 'אחרי הרענון יש לעדכן את הגדרות השלוחה בקו. האם להמשיך?',
124
+ callback: function(result) {
125
+ if (!result) return;
126
+ $.post(config.relative_path + '/api/admin/plugins/phone-verification/refresh-token', { _csrf: config.csrf_token }, function(res) {
127
+ if (res && res.success && res.token) {
128
+ $('#callApiToken').val(res.token);
129
+ alerts.success('הטוקן עודכן');
130
+ } else {
131
+ alerts.error('שגיאה ברענון הטוקן');
132
+ }
133
+ });
134
+ }
135
+ });
136
+ });
137
+
21
138
 
22
139
  function renderPagination(curr, total) {
23
140
  currentPage = curr;
@@ -133,6 +250,25 @@ define('admin/plugins/phone-verification', ['settings', 'bootbox', 'alerts'], fu
133
250
  });
134
251
  });
135
252
 
253
+ $('#test-user-call-btn').on('click', function() {
254
+ var phone = $('#test-user-call-phone').val();
255
+ if(!phone) return alerts.error('נא להזין מספר לבדיקה');
256
+
257
+ $.post(config.relative_path + '/api/admin/plugins/phone-verification/test-user-call', { phoneNumber: phone, _csrf: config.csrf_token }, function(res) {
258
+ if(res.success) {
259
+ var msg = 'קוד אימות נוצר בהצלחה';
260
+ if (res.code) {
261
+ msg += '<br><strong>קוד האימות: ' + res.code + '</strong>';
262
+ }
263
+ if (res.phoneNumber) {
264
+ msg += '<br><strong>מספר הקו: ' + res.phoneNumber + '</strong>';
265
+ }
266
+ alerts.success(msg);
267
+ }
268
+ else alerts.error(res.message || 'שגיאה ביצירת קוד האימות');
269
+ });
270
+ });
271
+
136
272
  $('#users-table').on('click', '.verify-user-btn', function() {
137
273
  var uid = $(this).data('uid');
138
274
  var name = $(this).data('name');
@@ -181,4 +317,4 @@ define('admin/plugins/phone-verification', ['settings', 'bootbox', 'alerts'], fu
181
317
  };
182
318
 
183
319
  return ACP;
184
- });
320
+ });
@@ -8,12 +8,32 @@ define('forum/phone-verification', ['hooks', 'translator'], function (hooks, tra
8
8
  return /^05\d{8}$/.test(cleanPhone);
9
9
  }
10
10
 
11
+ let publicSettingsCache = null;
12
+
13
+ function loadPublicSettings() {
14
+ if (publicSettingsCache) return $.Deferred().resolve(publicSettingsCache).promise();
15
+ return $.getJSON(config.relative_path + '/api/phone-verification/public-settings')
16
+ .then(function(res) {
17
+ if (res && res.success && res.settings) {
18
+ publicSettingsCache = res.settings;
19
+ } else {
20
+ publicSettingsCache = { voiceServerEnabled: true, userCallEnabled: false, userCallNumber: '' };
21
+ }
22
+ return publicSettingsCache;
23
+ })
24
+ .catch(function() {
25
+ publicSettingsCache = { voiceServerEnabled: true, userCallEnabled: false, userCallNumber: '' };
26
+ return publicSettingsCache;
27
+ });
28
+ }
29
+
11
30
  // ==================== לוגיקה לעמוד הרשמה (Registration) ====================
12
31
 
13
32
  const Registration = {
14
33
  resendTimer: null,
15
34
  resendCountdown: 0,
16
35
  phoneVerified: false,
36
+ registerBtn: null,
17
37
 
18
38
  validatePhone: function(phone) {
19
39
  return isValidIsraeliPhone(phone);
@@ -55,7 +75,9 @@ define('forum/phone-verification', ['hooks', 'translator'], function (hooks, tra
55
75
  if (this.resendCountdown > 0) {
56
76
  $btn.prop('disabled', true).text('שלח שוב (' + this.resendCountdown + ')');
57
77
  } else {
58
- $btn.prop('disabled', false).text('שלח צינתוק שוב');
78
+ const method = this.getSelectedMethod();
79
+ const label = method === 'user-call' ? 'הכן קוד שוב' : 'שלח צינתוק שוב';
80
+ $btn.prop('disabled', false).text(label);
59
81
  }
60
82
  },
61
83
 
@@ -66,9 +88,50 @@ define('forum/phone-verification', ['hooks', 'translator'], function (hooks, tra
66
88
  if ($('#phoneNumber').length) return;
67
89
 
68
90
  self.phoneVerified = false;
91
+ const $registerBtn = $form.find('button[type="submit"]');
92
+ self.registerBtn = $registerBtn;
93
+ if ($registerBtn.length) {
94
+ $registerBtn.prop('disabled', true);
95
+ }
96
+
97
+ loadPublicSettings().then(function(settings) {
98
+ const phoneHtml = self.buildPhoneHtml(settings);
99
+ if ($registerBtn.length) {
100
+ $(phoneHtml).insertBefore($registerBtn);
101
+ } else {
102
+ $form.append(phoneHtml);
103
+ }
104
+
105
+ self.attachEventListeners(settings);
106
+ self.checkExistingVerification();
107
+ });
108
+ },
109
+
110
+ buildPhoneHtml: function(settings) {
111
+ const showTzintuk = settings.voiceServerEnabled;
112
+ const showUserCall = settings.userCallEnabled;
113
+ const userCallNumber = settings.userCallNumber || '';
114
+ const methodsHtml = `
115
+ <div class="mb-2 d-flex flex-column gap-2 hidden" id="verification-methods">
116
+ <label class="form-label fw-bold">בחר שיטת אימות</label>
117
+ <div class="d-flex flex-column gap-1">
118
+ ${showTzintuk ? `
119
+ <label class="form-check-label">
120
+ <input class="form-check-input" type="radio" name="verificationMethod" value="tzintuk" />
121
+ אני רוצה לקבל את הקוד בצינתוק
122
+ </label>` : ''}
123
+ ${showUserCall ? `
124
+ <label class="form-check-label">
125
+ <input class="form-check-input" type="radio" name="verificationMethod" value="user-call" />
126
+ אני רוצה להתקשר לקו ולשמוע את הקוד
127
+ </label>` : ''}
128
+ </div>
129
+ <div class="form-text text-xs" id="method-help"></div>
130
+ <div class="form-text text-xs" id="user-call-number-text" ${userCallNumber ? '' : 'style="display:none;"'}>מספר הקו: <span dir="ltr">${userCallNumber}</span></div>
131
+ </div>
132
+ `;
69
133
 
70
- // עדכון טקסטים לצינתוק
71
- const phoneHtml = `
134
+ return `
72
135
  <div class="mb-2 d-flex flex-column gap-2" id="phone-verification-container">
73
136
  <label for="phoneNumber">מספר טלפון <span class="text-danger">*</span></label>
74
137
  <div class="d-flex flex-column">
@@ -79,7 +142,7 @@ define('forum/phone-verification', ['hooks', 'translator'], function (hooks, tra
79
142
  <i class="fa fa-phone"></i> שלח לאימות
80
143
  </button>
81
144
  </div>
82
- <span class="form-text text-xs">תקבל שיחה (צינתוק). קוד האימות הוא <strong>4 הספרות האחרונות</strong> של המספר המתקשר.</span>
145
+ ${methodsHtml}
83
146
  <div id="phone-error" class="text-danger text-xs hidden"></div>
84
147
  <div id="phone-success" class="text-success text-xs hidden"></div>
85
148
  </div>
@@ -105,28 +168,89 @@ define('forum/phone-verification', ['hooks', 'translator'], function (hooks, tra
105
168
  <i class="fa fa-check-circle"></i> מספר הטלפון אומת בהצלחה!
106
169
  </div>
107
170
  `;
108
-
109
- const $registerBtn = $form.find('button[type="submit"]');
110
- if ($registerBtn.length) {
111
- $(phoneHtml).insertBefore($registerBtn);
171
+ },
172
+
173
+ getSelectedMethod: function() {
174
+ const selected = $('input[name="verificationMethod"]:checked').val();
175
+ return selected || 'tzintuk';
176
+ },
177
+
178
+ updateMethodHelp: function(settings) {
179
+ const method = this.getSelectedMethod();
180
+ if (method === 'user-call') {
181
+ $('#method-help').text('לאחר לחיצה על שלח לאימות יש להתקשר לקו המוצג ולשמוע את קוד האימות.');
182
+ $('#verificationCode').attr('placeholder', 'קוד שהושמע בשיחה');
183
+ $('#user-call-number-text').toggle(!!(settings.userCallNumber && settings.userCallNumber.length));
112
184
  } else {
113
- $form.append(phoneHtml);
185
+ $('#method-help').text('תקבל שיחה (צינתוק). קוד האימות הוא 4 הספרות האחרונות של המספר המתקשר.');
186
+ $('#verificationCode').attr('placeholder', '4 ספרות אחרונות של המספר המחייג');
187
+ $('#user-call-number-text').hide();
114
188
  }
115
-
116
- self.attachEventListeners();
117
- self.checkExistingVerification();
118
189
  },
119
190
 
120
- attachEventListeners: function() {
191
+ attachEventListeners: function(settings) {
121
192
  const self = this;
122
193
 
123
- $('#send-code-btn').off('click').on('click', function () {
194
+ function showMethodsIfValid() {
195
+ const phone = $('#phoneNumber').val().trim();
196
+ if (self.validatePhone(phone)) {
197
+ $('#verification-methods').removeClass('hidden');
198
+ if ($('input[name="verificationMethod"]:checked').length === 0) {
199
+ if (settings.voiceServerEnabled) {
200
+ $('input[name="verificationMethod"][value="tzintuk"]').prop('checked', true);
201
+ } else if (settings.userCallEnabled) {
202
+ $('input[name="verificationMethod"][value="user-call"]').prop('checked', true);
203
+ }
204
+ }
205
+ self.updateMethodHelp(settings);
206
+ } else {
207
+ $('#verification-methods').addClass('hidden');
208
+ }
209
+ }
210
+
211
+ $('#phoneNumber').on('input blur', function() {
212
+ showMethodsIfValid();
213
+ });
214
+
215
+ $('body').on('change', 'input[name="verificationMethod"]', function() {
216
+ self.updateMethodHelp(settings);
217
+ self.updateResendButton();
218
+ });
219
+
220
+ function requestVerification(method) {
124
221
  const phone = $('#phoneNumber').val().trim();
125
222
  self.hideMessages();
126
-
127
- const $btn = $(this);
223
+
224
+ const $btn = $('#send-code-btn');
128
225
  $btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> שולח...');
129
-
226
+
227
+ if (method === 'user-call') {
228
+ $.ajax({
229
+ url: config.relative_path + '/api/phone-verification/request-user-call',
230
+ method: 'POST',
231
+ data: { phoneNumber: phone },
232
+ headers: { 'x-csrf-token': config.csrf_token },
233
+ success: function (response) {
234
+ if (response.success) {
235
+ var numberText = response.callNumber ? (' מספר הקו: ' + response.callNumber) : '';
236
+ self.showSuccess('הקוד הוכן. נא להתקשר לקו לשמיעת הקוד.' + numberText);
237
+ $('#verification-code-container').removeClass('hidden');
238
+ $('#phoneNumber').prop('readonly', true);
239
+ $btn.addClass('hidden');
240
+ self.startResendTimer();
241
+ } else {
242
+ self.showError(response.message || 'שגיאה בשליחה');
243
+ $btn.prop('disabled', false).html('<i class="fa fa-phone"></i> שלח לאימות');
244
+ }
245
+ },
246
+ error: function() {
247
+ self.showError('שגיאת תקשורת');
248
+ $btn.prop('disabled', false).html('<i class="fa fa-phone"></i> שלח לאימות');
249
+ }
250
+ });
251
+ return;
252
+ }
253
+
130
254
  $.ajax({
131
255
  url: config.relative_path + '/api/phone-verification/send-code',
132
256
  method: 'POST',
@@ -149,6 +273,11 @@ define('forum/phone-verification', ['hooks', 'translator'], function (hooks, tra
149
273
  $btn.prop('disabled', false).html('<i class="fa fa-phone"></i> שלח לאימות');
150
274
  }
151
275
  });
276
+ }
277
+
278
+ $('#send-code-btn').off('click').on('click', function () {
279
+ const method = self.getSelectedMethod();
280
+ requestVerification(method);
152
281
  });
153
282
 
154
283
  $('#verify-code-btn').off('click').on('click', function () {
@@ -178,6 +307,9 @@ define('forum/phone-verification', ['hooks', 'translator'], function (hooks, tra
178
307
  } else {
179
308
  $('#phoneNumberVerified').val(phone);
180
309
  }
310
+ if (self.registerBtn && self.registerBtn.length) {
311
+ self.registerBtn.prop('disabled', false);
312
+ }
181
313
  } else {
182
314
  self.showError(response.message || 'קוד שגוי');
183
315
  }
@@ -185,6 +317,13 @@ define('forum/phone-verification', ['hooks', 'translator'], function (hooks, tra
185
317
  });
186
318
  });
187
319
 
320
+ $('#resend-code-btn').off('click').on('click', function () {
321
+ if (self.resendCountdown > 0) return;
322
+ const method = self.getSelectedMethod();
323
+ $('#send-code-btn').removeClass('hidden');
324
+ requestVerification(method);
325
+ });
326
+
188
327
  $('[component="register/local"]').off('submit.phone').on('submit.phone', function (e) {
189
328
  if (!self.phoneVerified) {
190
329
  e.preventDefault();
@@ -259,7 +398,7 @@ define('forum/phone-verification', ['hooks', 'translator'], function (hooks, tra
259
398
  <i class="fa fa-info-circle"></i>
260
399
  ${isVerified
261
400
  ? 'המספר הנוכחי מאומת. שינוי המספר יחייב אימות מחדש.'
262
- : 'יש להזין מספר ולקבל צינתוק לאימות.'}
401
+ : 'יש להזין מספר ולקבל אימות.'}
263
402
  </div>
264
403
  </div>
265
404
  <div id="modal-alert-area"></div>
@@ -275,7 +414,7 @@ define('forum/phone-verification', ['hooks', 'translator'], function (hooks, tra
275
414
  className: 'btn-ghost'
276
415
  },
277
416
  verify: {
278
- label: 'שלח צינתוק',
417
+ label: 'המשך לאימות',
279
418
  className: 'btn-primary',
280
419
  callback: function() {
281
420
  const newPhone = $('#modal-phoneNumber').val();
@@ -308,20 +447,14 @@ define('forum/phone-verification', ['hooks', 'translator'], function (hooks, tra
308
447
  }, function(res) {
309
448
  if (!res.success) {
310
449
  showModalAlert(res.message || res.error, 'danger');
311
- $btn.prop('disabled', false).text('שלח צינתוק');
450
+ $btn.prop('disabled', false).text('המשך לאימות');
312
451
  return;
313
452
  }
314
453
 
315
- $.post(config.relative_path + '/api/phone-verification/send-code', {
316
- phoneNumber: phone,
317
- _csrf: config.csrf_token
318
- }, function(callRes) {
319
- if (callRes.success) {
320
- dialog.modal('hide');
321
-
322
- // עדכון ה-Prompt לקליטת 4 ספרות
454
+ loadPublicSettings().then(function(settings) {
455
+ function askForCode(title) {
323
456
  bootbox.prompt({
324
- title: "הזן את 4 הספרות האחרונות של המספר שחייג אליך כעת",
457
+ title: title,
325
458
  inputType: 'number',
326
459
  callback: function (code) {
327
460
  if (!code) return;
@@ -339,9 +472,62 @@ define('forum/phone-verification', ['hooks', 'translator'], function (hooks, tra
339
472
  });
340
473
  }
341
474
  });
475
+ }
476
+
477
+ function sendByMethod(method) {
478
+ if (method === 'user-call') {
479
+ $.post(config.relative_path + '/api/phone-verification/request-user-call', {
480
+ phoneNumber: phone,
481
+ _csrf: config.csrf_token
482
+ }, function(callRes) {
483
+ if (callRes.success) {
484
+ dialog.modal('hide');
485
+ var title = "התקשר לקו לשמיעת קוד האימות";
486
+ if (callRes.callNumber) title += " (" + callRes.callNumber + ")";
487
+ askForCode(title);
488
+ } else {
489
+ showModalAlert(callRes.message || 'שגיאה בהכנת הקוד', 'danger');
490
+ $btn.prop('disabled', false).text('המשך לאימות');
491
+ }
492
+ });
493
+ return;
494
+ }
495
+
496
+ $.post(config.relative_path + '/api/phone-verification/send-code', {
497
+ phoneNumber: phone,
498
+ _csrf: config.csrf_token
499
+ }, function(callRes) {
500
+ if (callRes.success) {
501
+ dialog.modal('hide');
502
+ askForCode("הזן את 4 הספרות האחרונות של המספר שחייג אליך כעת");
503
+ } else {
504
+ showModalAlert(callRes.message || 'שגיאה בשליחת הצינתוק', 'danger');
505
+ $btn.prop('disabled', false).text('המשך לאימות');
506
+ }
507
+ });
508
+ }
509
+
510
+ if (settings.voiceServerEnabled && settings.userCallEnabled) {
511
+ bootbox.dialog({
512
+ title: 'בחר שיטת אימות',
513
+ message: 'ניתן לבחור אימות בצינתוק או שיחה יזומה מצד המשתמש.',
514
+ buttons: {
515
+ tzintuk: {
516
+ label: 'צינתוק',
517
+ className: 'btn-primary',
518
+ callback: function() { sendByMethod('tzintuk'); }
519
+ },
520
+ userCall: {
521
+ label: 'שיחה יזומה',
522
+ className: 'btn-success',
523
+ callback: function() { sendByMethod('user-call'); }
524
+ }
525
+ }
526
+ });
527
+ } else if (settings.userCallEnabled) {
528
+ sendByMethod('user-call');
342
529
  } else {
343
- showModalAlert(callRes.message || 'שגיאה בשליחת הצינתוק', 'danger');
344
- $btn.prop('disabled', false).text('שלח צינתוק');
530
+ sendByMethod('tzintuk');
345
531
  }
346
532
  });
347
533
  });
@@ -432,4 +618,4 @@ define('forum/phone-verification', ['hooks', 'translator'], function (hooks, tra
432
618
  }
433
619
 
434
620
  return Plugin;
435
- });
621
+ });
@@ -14,10 +14,37 @@
14
14
  <div class="form-group">
15
15
  <label for="voiceServerEnabled">
16
16
  <input type="checkbox" id="voiceServerEnabled" name="voiceServerEnabled" />
17
- הפעל אימות בצינתוק
17
+ אימות ע"י צינתוק (0.1 יחידות)
18
18
  </label>
19
19
  <p class="help-block">כאשר מופעל, התוסף ישלח צינתוק (שיחה מנותקת) למשתמש, שיצטרך לאמת את 4 הספרות האחרונות של המספר המתקשר.</p>
20
20
  </div>
21
+
22
+ <div class="form-group">
23
+ <label for="userCallEnabled">
24
+ <input type="checkbox" id="userCallEnabled" name="userCallEnabled" />
25
+ אימות ע"י שיחה יזומה מצד המשתמש (חינם)
26
+ </label>
27
+ <p class="help-block">כאשר מופעל, המשתמש יקבל קוד אימות לאחר שיתקשר לקו שלכם.</p>
28
+ </div>
29
+
30
+ <div class="form-group">
31
+ <label for="userCallNumber">מספר הקו שיוצג למשתמשים</label>
32
+ <input type="text" class="form-control" id="userCallNumber" name="userCallNumber"
33
+ placeholder="לדוגמה: 03-1234567" dir="ltr" />
34
+ </div>
35
+
36
+ <div class="form-group">
37
+ <label for="callApiToken">טוקן ייחודי לפורום</label>
38
+ <div class="input-group">
39
+ <input type="text" class="form-control" id="callApiToken" name="callApiToken" readonly dir="ltr" />
40
+ <span class="input-group-btn">
41
+ <button type="button" class="btn btn-default" id="refresh-call-token-btn">
42
+ <i class="fa fa-refresh"></i> רענן טוקן
43
+ </button>
44
+ </span>
45
+ </div>
46
+ <p class="help-block">טוקן זה משמש לזיהוי הקו שלכם ב־API.</p>
47
+ </div>
21
48
 
22
49
  <div class="form-group">
23
50
  <label for="voiceServerApiKey">Token של Call2All</label>
@@ -67,16 +94,36 @@
67
94
 
68
95
  <hr />
69
96
 
70
- <h4>בדיקת צינתוק</h4>
71
- <div class="form-inline">
72
- <div class="form-group">
73
- <input type="text" class="form-control" id="test-phone"
74
- placeholder="05X-XXXXXXX" dir="ltr" style="width: 150px;" />
97
+ <div class="row">
98
+ <div class="col-md-6">
99
+ <h4>בדיקת צינתוק</h4>
100
+ <div class="form-inline">
101
+ <div class="form-group">
102
+ <input type="text" class="form-control" id="test-phone"
103
+ placeholder="05X-XXXXXXX" dir="ltr" style="width: 150px;" />
104
+ </div>
105
+ <button type="button" class="btn btn-warning" id="test-call-btn">
106
+ <i class="fa fa-phone"></i> שלח צינתוק בדיקה
107
+ </button>
108
+ <span id="test-status" style="margin-right: 10px;"></span>
109
+ </div>
110
+ </div>
111
+ <div class="col-md-6">
112
+ <h4>בדיקת שיחה יזומה</h4>
113
+ <div class="form-inline">
114
+ <div class="form-group">
115
+ <input type="text" class="form-control" id="test-user-call-phone"
116
+ placeholder="05X-XXXXXXX" dir="ltr" style="width: 150px;" />
117
+ </div>
118
+ <button type="button" class="btn btn-info" id="test-user-call-btn">
119
+ <i class="fa fa-phone-square"></i> בדוק שיחה יזומה
120
+ </button>
121
+ <span id="test-user-call-status" style="margin-right: 10px;"></span>
122
+ </div>
123
+ <p class="help-block" style="font-size: 12px; margin-top: 5px;">
124
+ יוצר קוד אימות זמני ומציג אותו למנהל לבדיקה
125
+ </p>
75
126
  </div>
76
- <button type="button" class="btn btn-warning" id="test-call-btn">
77
- <i class="fa fa-phone"></i> שלח צינתוק בדיקה
78
- </button>
79
- <span id="test-status" style="margin-right: 10px;"></span>
80
127
  </div>
81
128
  </div>
82
129
  </div>
@@ -147,4 +194,4 @@
147
194
  </div>
148
195
  </div>
149
196
  </div>
150
- </div>
197
+ </div>
@@ -14,10 +14,37 @@
14
14
  <div class="form-group">
15
15
  <label for="voiceServerEnabled">
16
16
  <input type="checkbox" id="voiceServerEnabled" name="voiceServerEnabled" />
17
- הפעל אימות בצינתוק
17
+ אימות ע"י צינתוק (0.1 יחידות)
18
18
  </label>
19
19
  <p class="help-block">כאשר מופעל, התוסף ישלח צינתוק (שיחה מנותקת) למשתמש, שיצטרך לאמת את 4 הספרות האחרונות של המספר המתקשר.</p>
20
20
  </div>
21
+
22
+ <div class="form-group">
23
+ <label for="userCallEnabled">
24
+ <input type="checkbox" id="userCallEnabled" name="userCallEnabled" />
25
+ אימות ע"י שיחה יזומה מצד המשתמש (חינם)
26
+ </label>
27
+ <p class="help-block">כאשר מופעל, המשתמש יקבל קוד אימות לאחר שיתקשר לקו שלכם.</p>
28
+ </div>
29
+
30
+ <div class="form-group">
31
+ <label for="userCallNumber">מספר הקו שיוצג למשתמשים</label>
32
+ <input type="text" class="form-control" id="userCallNumber" name="userCallNumber"
33
+ placeholder="לדוגמה: 03-1234567" dir="ltr" />
34
+ </div>
35
+
36
+ <div class="form-group">
37
+ <label for="callApiToken">טוקן ייחודי לפורום</label>
38
+ <div class="input-group">
39
+ <input type="text" class="form-control" id="callApiToken" name="callApiToken" readonly dir="ltr" />
40
+ <span class="input-group-btn">
41
+ <button type="button" class="btn btn-default" id="refresh-call-token-btn">
42
+ <i class="fa fa-refresh"></i> רענן טוקן
43
+ </button>
44
+ </span>
45
+ </div>
46
+ <p class="help-block">טוקן זה משמש לזיהוי הקו שלכם ב־API.</p>
47
+ </div>
21
48
 
22
49
  <div class="form-group">
23
50
  <label for="voiceServerApiKey">Token של Call2All</label>
@@ -67,16 +94,36 @@
67
94
 
68
95
  <hr />
69
96
 
70
- <h4>בדיקת צינתוק</h4>
71
- <div class="form-inline">
72
- <div class="form-group">
73
- <input type="text" class="form-control" id="test-phone"
74
- placeholder="05X-XXXXXXX" dir="ltr" style="width: 150px;" />
97
+ <div class="row">
98
+ <div class="col-md-6">
99
+ <h4>בדיקת צינתוק</h4>
100
+ <div class="form-inline">
101
+ <div class="form-group">
102
+ <input type="text" class="form-control" id="test-phone"
103
+ placeholder="05X-XXXXXXX" dir="ltr" style="width: 150px;" />
104
+ </div>
105
+ <button type="button" class="btn btn-warning" id="test-call-btn">
106
+ <i class="fa fa-phone"></i> שלח צינתוק בדיקה
107
+ </button>
108
+ <span id="test-status" style="margin-right: 10px;"></span>
109
+ </div>
110
+ </div>
111
+ <div class="col-md-6">
112
+ <h4>בדיקת שיחה יזומה</h4>
113
+ <div class="form-inline">
114
+ <div class="form-group">
115
+ <input type="text" class="form-control" id="test-user-call-phone"
116
+ placeholder="05X-XXXXXXX" dir="ltr" style="width: 150px;" />
117
+ </div>
118
+ <button type="button" class="btn btn-info" id="test-user-call-btn">
119
+ <i class="fa fa-phone-square"></i> בדוק שיחה יזומה
120
+ </button>
121
+ <span id="test-user-call-status" style="margin-right: 10px;"></span>
122
+ </div>
123
+ <p class="help-block" style="font-size: 12px; margin-top: 5px;">
124
+ יוצר קוד אימות זמני ומציג אותו למנהל לבדיקה
125
+ </p>
75
126
  </div>
76
- <button type="button" class="btn btn-warning" id="test-call-btn">
77
- <i class="fa fa-phone"></i> שלח צינתוק בדיקה
78
- </button>
79
- <span id="test-status" style="margin-right: 10px;"></span>
80
127
  </div>
81
128
  </div>
82
129
  </div>
@@ -147,4 +194,4 @@
147
194
  </div>
148
195
  </div>
149
196
  </div>
150
- </div>
197
+ </div>