Mbonea commited on
Commit
d8fc0a7
·
1 Parent(s): 3434a8c

Alert tenants for token lifecycle events

Browse files
src/jobs/pollSessions.job.js CHANGED
@@ -1,6 +1,7 @@
1
  const db = require('../config/db');
2
  const omada = require('../services/omada');
3
  const { alertAdmin } = require('../utils/adminAlert');
 
4
 
5
  function normalizeMac(mac) {
6
  return String(mac || '')
@@ -111,6 +112,8 @@ async function pollSessions() {
111
 
112
  if (!token) {
113
  console.log(`[pollSessions] Created manual Omada session for mac=${mac} device=${device.id} without token`);
 
 
114
  }
115
  }
116
  } catch (err) {
 
1
  const db = require('../config/db');
2
  const omada = require('../services/omada');
3
  const { alertAdmin } = require('../utils/adminAlert');
4
+ const { alertWifiClient } = require('../utils/tokenClientAlerts');
5
 
6
  function normalizeMac(mac) {
7
  return String(mac || '')
 
112
 
113
  if (!token) {
114
  console.log(`[pollSessions] Created manual Omada session for mac=${mac} device=${device.id} without token`);
115
+ } else {
116
+ alertWifiClient('token_started', token.id, { mac }).catch(() => {});
117
  }
118
  }
119
  } catch (err) {
src/routes/portal.routes.js CHANGED
@@ -12,6 +12,7 @@ const { alertAdmin } = require('../utils/adminAlert');
12
  const { makePaymentAssetUrl } = require('../utils/portalPaymentStorage');
13
  const { snippeWebhookUrl } = require('../utils/webhookUrl');
14
  const { decryptSecret } = require('../utils/paymentSecrets');
 
15
 
16
  const normalizeMac = mac => String(mac || '').toUpperCase().replace(/[:\-]/g, '');
17
  const normalizePortalMac = mac => String(mac || '').trim().toUpperCase().replace(/:/g, '-');
@@ -175,6 +176,7 @@ async function recordOmadaVoucherUse({ siteId, code, mac }) {
175
 
176
  const now = new Date();
177
  let expiresAt = token.expires_at ? new Date(token.expires_at) : null;
 
178
 
179
  if (token.status === 'revoked') {
180
  await conn.rollback();
@@ -245,6 +247,10 @@ async function recordOmadaVoucherUse({ siteId, code, mac }) {
245
 
246
  await conn.commit();
247
 
 
 
 
 
248
  return {
249
  token: updatedRows[0] || { ...token, status: 'active', locked_mac: mac, expires_at: expiresAt },
250
  expiresAt,
@@ -882,6 +888,7 @@ router.post('/:siteId/auth', portalAuthLimiter, async (req, res) => {
882
 
883
  const now = new Date();
884
  let expiresAt = token.expires_at ? new Date(token.expires_at) : null;
 
885
  trace.step('expiry_window_prepared', { existingExpiresAt: token.expires_at });
886
 
887
  if (token.status === 'unused') {
@@ -897,6 +904,7 @@ router.post('/:siteId/auth', portalAuthLimiter, async (req, res) => {
897
  [mac, now, expiresAt, token.id]
898
  );
899
  trace.step('token_activated', { expiresAt: expiresAt.toISOString() });
 
900
  } else if (!expiresAt) {
901
  expiresAt = new Date(now.getTime() + token.duration_seconds * 1000);
902
  await db.query(
@@ -929,6 +937,10 @@ router.post('/:siteId/auth', portalAuthLimiter, async (req, res) => {
929
  trace.step('session_created');
930
  }
931
 
 
 
 
 
932
  const unitLabel = (v, u) => `${v}${u === 2 ? 'Mbps' : 'Kbps'}`;
933
  const secsLeft = Math.floor((expiresAt - now) / 1000);
934
  trace.step('response_ready', { secsLeft, expiresAt: expiresAt.toISOString() });
 
12
  const { makePaymentAssetUrl } = require('../utils/portalPaymentStorage');
13
  const { snippeWebhookUrl } = require('../utils/webhookUrl');
14
  const { decryptSecret } = require('../utils/paymentSecrets');
15
+ const { alertWifiClient } = require('../utils/tokenClientAlerts');
16
 
17
  const normalizeMac = mac => String(mac || '').toUpperCase().replace(/[:\-]/g, '');
18
  const normalizePortalMac = mac => String(mac || '').trim().toUpperCase().replace(/:/g, '-');
 
176
 
177
  const now = new Date();
178
  let expiresAt = token.expires_at ? new Date(token.expires_at) : null;
179
+ const shouldAlertStarted = token.status === 'unused' || !token.activated_at;
180
 
181
  if (token.status === 'revoked') {
182
  await conn.rollback();
 
247
 
248
  await conn.commit();
249
 
250
+ if (shouldAlertStarted) {
251
+ alertWifiClient('token_started', token.id, { mac }).catch(() => {});
252
+ }
253
+
254
  return {
255
  token: updatedRows[0] || { ...token, status: 'active', locked_mac: mac, expires_at: expiresAt },
256
  expiresAt,
 
888
 
889
  const now = new Date();
890
  let expiresAt = token.expires_at ? new Date(token.expires_at) : null;
891
+ let shouldAlertStarted = false;
892
  trace.step('expiry_window_prepared', { existingExpiresAt: token.expires_at });
893
 
894
  if (token.status === 'unused') {
 
904
  [mac, now, expiresAt, token.id]
905
  );
906
  trace.step('token_activated', { expiresAt: expiresAt.toISOString() });
907
+ shouldAlertStarted = true;
908
  } else if (!expiresAt) {
909
  expiresAt = new Date(now.getTime() + token.duration_seconds * 1000);
910
  await db.query(
 
937
  trace.step('session_created');
938
  }
939
 
940
+ if (shouldAlertStarted) {
941
+ alertWifiClient('token_started', token.id, { mac }).catch(() => {});
942
+ }
943
+
944
  const unitLabel = (v, u) => `${v}${u === 2 ? 'Mbps' : 'Kbps'}`;
945
  const secsLeft = Math.floor((expiresAt - now) / 1000);
946
  trace.step('response_ready', { secsLeft, expiresAt: expiresAt.toISOString() });
src/routes/tokens.routes.js CHANGED
@@ -2,6 +2,7 @@ const router = require('express').Router();
2
  const db = require('../config/db');
3
  const { requireAuth } = require('../middleware/auth');
4
  const omada = require('../services/omada');
 
5
 
6
  router.use(requireAuth);
7
 
@@ -650,6 +651,10 @@ router.post('/:id/extend', async (req, res) => {
650
  await connection.commit();
651
 
652
  const updated = await loadTokenRow(token.token_id, req.client.id);
 
 
 
 
653
 
654
  res.json({
655
  success: true,
@@ -770,6 +775,12 @@ router.patch('/:id/duration', async (req, res) => {
770
  await connection.commit();
771
 
772
  const updated = await loadTokenRow(token.token_id, req.client.id);
 
 
 
 
 
 
773
 
774
  res.json({
775
  success: true,
@@ -911,6 +922,13 @@ router.delete('/:id', async (req, res) => {
911
 
912
  await connection.commit();
913
 
 
 
 
 
 
 
 
914
  res.json({
915
  success: true,
916
  action,
 
2
  const db = require('../config/db');
3
  const { requireAuth } = require('../middleware/auth');
4
  const omada = require('../services/omada');
5
+ const { alertWifiClient } = require('../utils/tokenClientAlerts');
6
 
7
  router.use(requireAuth);
8
 
 
651
  await connection.commit();
652
 
653
  const updated = await loadTokenRow(token.token_id, req.client.id);
654
+ alertWifiClient('token_extended', token.token_id, {
655
+ minutes: Math.ceil(grantedSeconds / 60),
656
+ expiresAt: auditNewExpiresAt,
657
+ }).catch(() => {});
658
 
659
  res.json({
660
  success: true,
 
775
  await connection.commit();
776
 
777
  const updated = await loadTokenRow(token.token_id, req.client.id);
778
+ if (deltaSeconds > 0) {
779
+ alertWifiClient('token_extended', token.token_id, {
780
+ minutes: Math.ceil(deltaSeconds / 60),
781
+ expiresAt: nextExpiresAt,
782
+ }).catch(() => {});
783
+ }
784
 
785
  res.json({
786
  success: true,
 
922
 
923
  await connection.commit();
924
 
925
+ if (action !== 'deleted') {
926
+ alertWifiClient('token_revoked', token.token_id, {
927
+ action,
928
+ mac: token.locked_mac,
929
+ }).catch(() => {});
930
+ }
931
+
932
  res.json({
933
  success: true,
934
  action,
src/utils/messages.js CHANGED
@@ -18,6 +18,18 @@ module.exports = {
18
  ? `Your WiFi access for ${deviceName} (${planName}) has expired. Buy a new bundle to reconnect.`
19
  : `Your WiFi access for ${deviceName} has expired. Buy a new bundle to reconnect.`,
20
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  // Tenant receives this after their device subscription renews
22
  deviceRenewal: (deviceName, expiryDate) =>
23
  `"${deviceName}" renewed until ${expiryDate}. Keep selling!`,
 
18
  ? `Your WiFi access for ${deviceName} (${planName}) has expired. Buy a new bundle to reconnect.`
19
  : `Your WiFi access for ${deviceName} has expired. Buy a new bundle to reconnect.`,
20
 
21
+ // Tenant receives this when a guest starts using a token
22
+ tokenStartedAlert: (code, deviceName, mac) =>
23
+ `Token ${code} started being used on ${deviceName}${mac ? ` by ${mac}` : ''}.`,
24
+
25
+ // Tenant receives this when a token extension is granted
26
+ tokenExtendedAlert: (code, deviceName, minutes, expiresAt = null) =>
27
+ `Token ${code} on ${deviceName} was extended by ${minutes} minute(s)${expiresAt ? `. New expiry: ${expiresAt}` : ''}.`,
28
+
29
+ // Tenant receives this when a token is revoked or voided
30
+ tokenRevokedAlert: (code, deviceName, action = 'revoked') =>
31
+ `Token ${code} on ${deviceName} was ${action}.`,
32
+
33
  // Tenant receives this after their device subscription renews
34
  deviceRenewal: (deviceName, expiryDate) =>
35
  `"${deviceName}" renewed until ${expiryDate}. Keep selling!`,
src/utils/tokenClientAlerts.js ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const db = require('../config/db');
2
+ const sms = require('../services/sms');
3
+ const messages = require('./messages');
4
+ const { canSendTenantNotification } = require('./notificationPreferences');
5
+
6
+ function formatDateTime(value) {
7
+ if (!value) return null;
8
+ const date = new Date(value);
9
+ if (Number.isNaN(date.getTime())) return null;
10
+ return date.toLocaleString('en-TZ', {
11
+ timeZone: 'Africa/Dar_es_Salaam',
12
+ year: 'numeric',
13
+ month: 'short',
14
+ day: '2-digit',
15
+ hour: '2-digit',
16
+ minute: '2-digit',
17
+ });
18
+ }
19
+
20
+ async function loadTokenContext(tokenId) {
21
+ return db.queryOne(
22
+ `SELECT t.id AS token_id, t.code AS token_code, t.locked_mac, t.expires_at,
23
+ d.id AS device_id, d.name AS device_name,
24
+ c.id AS client_id, c.phone AS client_phone
25
+ FROM access_tokens t
26
+ JOIN devices d ON d.id = t.device_id
27
+ JOIN clients c ON c.id = t.client_id
28
+ WHERE t.id = ?
29
+ LIMIT 1`,
30
+ [tokenId]
31
+ );
32
+ }
33
+
34
+ async function alertWifiClient(eventType, tokenId, options = {}) {
35
+ try {
36
+ const token = await loadTokenContext(tokenId);
37
+ if (!token?.client_id) return false;
38
+ if (!(await canSendTenantNotification(token.client_id, 'purchase_alerts'))) return false;
39
+
40
+ const mac = options.mac || token.locked_mac || null;
41
+ const minutes = options.minutes || null;
42
+ const action = options.action || 'revoked';
43
+ const expiresAt = formatDateTime(options.expiresAt || token.expires_at);
44
+
45
+ let alertKey = null;
46
+ let module = 'tokens';
47
+ let content = null;
48
+
49
+ if (eventType === 'token_started') {
50
+ alertKey = `token-started-${token.token_id}-${String(mac || 'unknown').replace(/[^A-Za-z0-9]/g, '')}`;
51
+ content = messages.tokenStartedAlert(token.token_code, token.device_name, mac);
52
+ } else if (eventType === 'token_extended') {
53
+ alertKey = `token-extended-${token.token_id}-${Date.now()}`;
54
+ content = messages.tokenExtendedAlert(token.token_code, token.device_name, minutes, expiresAt);
55
+ } else if (eventType === 'token_revoked') {
56
+ alertKey = `token-revoked-${token.token_id}-${Date.now()}`;
57
+ content = messages.tokenRevokedAlert(token.token_code, token.device_name, action);
58
+ } else {
59
+ return false;
60
+ }
61
+
62
+ const result = await db.query(
63
+ `INSERT IGNORE INTO omada_alerts
64
+ (client_id, device_id, omada_alert_id, alert_key, module, level, content, device_mac, alert_time)
65
+ VALUES (?, ?, ?, ?, ?, 'info', ?, ?, NOW())`,
66
+ [
67
+ token.client_id,
68
+ token.device_id,
69
+ alertKey,
70
+ eventType,
71
+ module,
72
+ content,
73
+ mac,
74
+ ]
75
+ );
76
+
77
+ if (Number(result?.affectedRows || 0) === 0) {
78
+ return false;
79
+ }
80
+
81
+ if (token.client_phone) {
82
+ sms.sendSMS(token.client_phone, content).catch(err => {
83
+ console.error('[token-alert] SMS failed:', err.message);
84
+ });
85
+ }
86
+
87
+ return true;
88
+ } catch (err) {
89
+ console.error('[token-alert] failed:', err.message, { eventType, tokenId });
90
+ return false;
91
+ }
92
+ }
93
+
94
+ module.exports = {
95
+ alertWifiClient,
96
+ };