nodebb-plugin-anti-account-sharing 1.0.5 → 1.0.6

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
@@ -8,23 +8,10 @@ const PLUGIN_ID = 'nodebb-plugin-anti-account-sharing';
8
8
 
9
9
  const DEFAULTS = {
10
10
  enabled: true,
11
-
12
- // Fingerprint değişince ne yapalım?
13
- // "kick" => diğer oturumları kır
14
- // "block" => isteği 403 ile engelle (giriş yapanı bile)
15
11
  enforcement: 'kick',
16
-
17
- // Aynı kullanıcı için fingerprint değişimini kaç saniye "şüpheli" sayalım?
18
- // Örn: 300 => son 5 dakikada farklı fingerprint görürse tetikle
19
12
  windowSeconds: 300,
20
-
21
- // Log seviyesi: "debug" | "info" | "warn" | "error"
22
13
  logLevel: 'info',
23
-
24
- // Fingerprint: ip + user-agent (+ accept-language opsiyonel)
25
14
  includeAcceptLanguage: false,
26
-
27
- // Reverse-proxy arkasında doğru ip için
28
15
  trustProxy: true,
29
16
  };
30
17
 
@@ -40,8 +27,6 @@ function toInt(v, dflt) {
40
27
  }
41
28
 
42
29
  async function getSettings() {
43
- // ACP -> Settings -> Anti Account Sharing (meta settings) varsa buradan okur
44
- // Yoksa defaults kullanır.
45
30
  let s = {};
46
31
  try {
47
32
  s = await meta.settings.get(PLUGIN_ID);
@@ -70,7 +55,6 @@ function sha1(input) {
70
55
  }
71
56
 
72
57
  function getClientIp(req, trustProxy) {
73
- // NodeBB/Express: trust proxy açık değilse x-forwarded-for güvenilmez olur.
74
58
  if (trustProxy) {
75
59
  const xff = (req.headers['x-forwarded-for'] || '').toString();
76
60
  if (xff) return xff.split(',')[0].trim();
@@ -91,17 +75,18 @@ function getFingerprint(req, settings) {
91
75
  return sha1(raw);
92
76
  }
93
77
 
78
+ function safeJson(x) {
79
+ try { return JSON.stringify(x); } catch (e) { return '[unjsonable]'; }
80
+ }
81
+
94
82
  function buildLogger(settings) {
95
- // winston ile NodeBB loglarına düşer, ayrıca console’a da basar.
96
83
  const level = settings?.logLevel || 'info';
97
84
 
98
85
  function log(lvl, msg, metaObj) {
99
86
  const line = `[AAS] ${msg}${metaObj ? ` ${safeJson(metaObj)}` : ''}`;
100
- // NodeBB winston
101
87
  if (winston && typeof winston[lvl] === 'function') winston[lvl](line);
102
88
  else if (winston && typeof winston.info === 'function') winston.info(line);
103
89
 
104
- // console fallback (docker logs)
105
90
  if (lvl === 'error') console.error(line);
106
91
  else if (lvl === 'warn') console.warn(line);
107
92
  else console.log(line);
@@ -115,11 +100,14 @@ function buildLogger(settings) {
115
100
  };
116
101
  }
117
102
 
118
- function safeJson(x) {
119
- try { return JSON.stringify(x); } catch (e) { return '[unjsonable]'; }
120
- }
103
+ /**
104
+ * IMPORTANT: Hook'ları burada export et
105
+ * security.js içinde bunlar tanımlı olacak.
106
+ */
107
+ const Security = require('./security');
121
108
 
122
109
  module.exports = {
110
+ // helpers
123
111
  PLUGIN_ID,
124
112
  DEFAULTS,
125
113
  getSettings,
@@ -127,4 +115,9 @@ module.exports = {
127
115
  getClientIp,
128
116
  sha1,
129
117
  buildLogger,
130
- };
118
+
119
+ // hooks (plugin.json bunları arıyor)
120
+ init: Security.init,
121
+ recordSession: Security.recordSession,
122
+ checkSession: Security.checkSession,
123
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-anti-account-sharing",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Prevents account sharing by enforcing a single active session policy for desktop devices.",
5
5
  "main": "library.js",
6
6
  "keywords": [
@@ -27,7 +27,6 @@ function sessionObjectKeys(sid) {
27
27
  }
28
28
 
29
29
  async function getUserSessionIds(uid) {
30
- // NodeBB genelde sorted set tutar
31
30
  try {
32
31
  const sids = await db.getSortedSetRange(SESS_SET(uid), 0, -1);
33
32
  return Array.isArray(sids) ? sids : [];
@@ -36,8 +35,7 @@ async function getUserSessionIds(uid) {
36
35
  }
37
36
  }
38
37
 
39
- async function deleteSession(sid, logger) {
40
- // session obj key’leri silmeyi dener
38
+ async function deleteSessionObjects(sid, logger) {
41
39
  for (const k of sessionObjectKeys(sid)) {
42
40
  try {
43
41
  await db.delete(k);
@@ -53,6 +51,7 @@ async function kickOtherSessions(uid, keepSid, logger) {
53
51
  if (!all.length) return { total: 0, removed: 0 };
54
52
 
55
53
  let removed = 0;
54
+
56
55
  for (const sid of all) {
57
56
  if (!sid) continue;
58
57
  if (keepSid && sid === keepSid) continue;
@@ -65,8 +64,9 @@ async function kickOtherSessions(uid, keepSid, logger) {
65
64
  logger.warn('Failed removing session from sessions set', { uid, sid, err: e.message });
66
65
  }
67
66
 
68
- await deleteSession(sid, logger);
67
+ await deleteSessionObjects(sid, logger);
69
68
  }
69
+
70
70
  return { total: all.length, removed };
71
71
  }
72
72
 
@@ -76,7 +76,6 @@ function getCurrentSessionId(req) {
76
76
 
77
77
  // Cookie’den yakalamayı dene (connect.sid vb.)
78
78
  const cookie = (req.headers && req.headers.cookie) ? String(req.headers.cookie) : '';
79
- // connect.sid=s%3Axxxxx.yyyyy
80
79
  const m = cookie.match(/connect\.sid=([^;]+)/i);
81
80
  if (m && m[1]) return decodeURIComponent(m[1]).replace(/^s:/, '').split('.')[0];
82
81
  return null;
@@ -100,6 +99,7 @@ async function shouldTrigger(uid, fp, nowSec, settings) {
100
99
  if (diff <= settings.windowSeconds) {
101
100
  return { trigger: true, reason: `fp_changed_within_${settings.windowSeconds}s` };
102
101
  }
102
+
103
103
  return { trigger: false, reason: `fp_changed_outside_window(${diff}s)` };
104
104
  }
105
105
 
@@ -124,11 +124,6 @@ function createSecurityMiddleware(settings, logger) {
124
124
  const uid = parseInt(req.uid, 10);
125
125
  if (!Number.isFinite(uid) || uid <= 0) return next();
126
126
 
127
- // Bazı route’larda user obj yoksa da problem değil
128
- // (Bunu loglamak istersen aç)
129
- // const u = await user.getUserFields(uid, ['username']);
130
- // const username = u?.username;
131
-
132
127
  const fp = getFingerprint(req, settings);
133
128
  const nowSec = Math.floor(Date.now() / 1000);
134
129
 
@@ -145,20 +140,16 @@ function createSecurityMiddleware(settings, logger) {
145
140
  });
146
141
 
147
142
  if (settings.enforcement === 'block') {
148
- // current request’i de engelle
149
143
  await updateLast(uid, fp, req, nowSec);
150
- res.status(403).json({
144
+ return res.status(403).json({
151
145
  error: 'account-sharing-detected',
152
146
  message: 'Bu hesap farklı cihaz/ağ üzerinden aynı anda kullanılıyor olabilir.',
153
147
  });
154
- return;
155
148
  }
156
149
 
157
150
  // kick mode: diğer session’ları kır
158
151
  const result = await kickOtherSessions(uid, keepSid, logger);
159
152
  logger.info('Kick result', { uid, ...result });
160
-
161
- // (İsteğe bağlı) burada kullanıcıya bildirim de basılabilir
162
153
  }
163
154
 
164
155
  // her request sonunda last’i güncelle
@@ -174,8 +165,7 @@ function createSecurityMiddleware(settings, logger) {
174
165
  const Security = {};
175
166
 
176
167
  Security.init = async function (params) {
177
- // NodeBB hook: static:app.load (en yaygın)
178
- // params: { app, router, middleware, controllers }
168
+ // NodeBB hook: static:app.load
179
169
  const { app } = params;
180
170
  const settings = await getSettings();
181
171
  const logger = buildLogger(settings);
@@ -192,14 +182,56 @@ Security.init = async function (params) {
192
182
  return;
193
183
  }
194
184
 
195
- // trust proxy opsiyonel (reverse proxy varsa doğru ip için)
185
+ // Reverse proxy varsa gerçek IP için
196
186
  try {
197
187
  app.set('trust proxy', !!settings.trustProxy);
198
188
  } catch (e) {}
199
189
 
200
- // NodeBB’nin auth middleware’inden sonra çalışması için genelde app.use yeterli
201
- app.use(createSecurityMiddleware(settings, logger));
190
+ // DEBUG ENDPOINT
191
+ // Sadece login olmuş kullanıcı görür (istersen admin-only yap)
192
+ app.get('/api/aas/debug', async (req, res) => {
193
+ try {
194
+ const uid = parseInt(req.uid, 10) || 0;
195
+ if (uid <= 0) {
196
+ return res.status(401).json({ ok: false, error: 'not-logged-in' });
197
+ }
202
198
 
199
+ // ✅ opsiyonel: sadece admin
200
+ // const isAdmin = await user.isAdministrator(uid);
201
+ // if (!isAdmin) return res.status(403).json({ ok: false, error: 'admin-only' });
202
+
203
+ const fp = getFingerprint(req, settings);
204
+ const sid = req.sessionID;
205
+
206
+ const sessions = await db.getSortedSetRange(`uid:${uid}:sessions`, 0, -1);
207
+
208
+ logger.info('DEBUG endpoint hit', {
209
+ uid,
210
+ sid,
211
+ fp,
212
+ sessionsCount: sessions.length,
213
+ });
214
+
215
+ return res.json({
216
+ ok: true,
217
+ plugin: PLUGIN_ID,
218
+ uid,
219
+ sessionID: sid,
220
+ fingerprint: fp,
221
+ sessions,
222
+ headers: {
223
+ ua: req.headers['user-agent'],
224
+ ip: req.headers['x-forwarded-for'] || req.ip,
225
+ },
226
+ });
227
+ } catch (e) {
228
+ logger.error('DEBUG endpoint error', { err: e.message });
229
+ return res.status(500).json({ error: e.message });
230
+ }
231
+ });
232
+
233
+ // ✅ middleware attach
234
+ app.use(createSecurityMiddleware(settings, logger));
203
235
  logger.info('Security middleware attached', { plugin: PLUGIN_ID });
204
236
  };
205
237