nodebb-plugin-anti-account-sharing 1.0.1 → 1.0.3

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
@@ -6,7 +6,7 @@ const user = require.main.require('./src/user');
6
6
  const Plugin = {};
7
7
 
8
8
  // --- AYARLAR ---
9
- const MAX_DEVICES = 1; // Kaç bilgisayara izin verilsin?
9
+ const MAX_DEVICES = 1; // kaç bilgisayar?
10
10
 
11
11
  function isMobile(req) {
12
12
  const ua = req?.headers?.['user-agent'] || '';
@@ -14,52 +14,71 @@ function isMobile(req) {
14
14
  }
15
15
 
16
16
  Plugin.init = async function () {
17
- console.log(`[Anti-Share] Güvenlik Aktif (Limit: ${MAX_DEVICES} Bilgisayar) 🛡️`);
17
+ console.log(`[Anti-Share] Aktif (Limit: ${MAX_DEVICES} PC)`);
18
18
  };
19
19
 
20
- // 1) Login olunca session kaydet
20
+ // Login olduğunda session ekle (req gelmezse bile sorun değil, aşağıda fallback var)
21
21
  Plugin.recordSession = async function (data) {
22
22
  const req = data?.req;
23
23
  const uid = data?.uid;
24
24
 
25
- // req yoksa sessizce çık (bazı akışlarda gelemeyebiliyor)
26
25
  if (!req || !uid || !req.sessionID) return;
27
-
28
- // Mobil girişleri takip etmiyoruz
29
26
  if (isMobile(req)) return;
30
27
 
31
- const key = `antishare:sessions:${uid}`;
28
+ const key = `antishare:sessionsz:${uid}`; // <-- DİKKAT: yeni key (zset)
29
+ const sid = req.sessionID;
30
+
31
+ // Eğer yoksa ekle (varsa score'u güncelleme!)
32
+ const all = await db.getSortedSetRevRange(key, 0, -1);
33
+ if (!all.includes(sid)) {
34
+ await db.sortedSetAdd(key, Date.now(), sid);
35
+ }
32
36
 
33
- // Yeni oturumu sona ekle, sonra sadece son MAX_DEVICES kadarını tut
34
- await db.listAppend(key, req.sessionID);
35
- await db.listTrim(key, -MAX_DEVICES, -1);
37
+ // sadece son MAX_DEVICES tut
38
+ const newest = await db.getSortedSetRevRange(key, 0, -1);
39
+ const toRemove = newest.slice(MAX_DEVICES);
40
+ if (toRemove.length) {
41
+ await db.sortedSetRemove(key, toRemove);
42
+ }
36
43
  };
37
44
 
38
- // 2) Her page + api isteğinde kontrol
45
+ // Her page + api isteğinde kontrol
39
46
  Plugin.checkSession = async function (data) {
40
47
  const req = data?.req;
41
48
  const res = data?.res;
42
49
 
43
50
  if (!req || !res) return data;
44
-
45
- // login değilse veya mobilse karışma
46
51
  if (!req.uid || req.uid <= 0 || isMobile(req)) return data;
47
52
 
48
- // admin ise karışma
49
53
  const isAdmin = await user.isAdministrator(req.uid);
50
54
  if (isAdmin) return data;
51
55
 
52
- const key = `antishare:sessions:${req.uid}`;
53
- const validSessions = await db.getListRange(key, 0, -1);
56
+ const sid = req.sessionID;
57
+ if (!sid) return data;
54
58
 
55
- // liste boşsa (ilk login vs.) karışma
56
- if (!Array.isArray(validSessions) || !validSessions.length) return data;
59
+ const key = `antishare:sessionsz:${req.uid}`; // <-- yeni key
60
+
61
+ // allowed = en yeni MAX_DEVICES session
62
+ let newest = await db.getSortedSetRevRange(key, 0, -1);
63
+
64
+ // Fallback: eğer hiç kayıt yoksa veya bu sid listede yoksa (ilk defa görüyorsak) ekle
65
+ // (AMA mevcut sid varsa score'u güncelleme! yoksa eski session kendini "yeniler" ve kick yemez)
66
+ if (!newest.includes(sid)) {
67
+ await db.sortedSetAdd(key, Date.now(), sid);
68
+ newest = await db.getSortedSetRevRange(key, 0, -1);
69
+ }
70
+
71
+ // Trim (fazla varsa sil)
72
+ const toRemove = newest.slice(MAX_DEVICES);
73
+ if (toRemove.length) {
74
+ await db.sortedSetRemove(key, toRemove);
75
+ newest = newest.slice(0, MAX_DEVICES);
76
+ }
57
77
 
58
- // sessionID yoksa karışma
59
- if (!req.sessionID) return data;
78
+ const allowed = newest; // en yeni MAX_DEVICES
60
79
 
61
- if (!validSessions.includes(req.sessionID)) {
62
- // Oturumu öldür
80
+ // Bu session allowed değilse -> kick
81
+ if (!allowed.includes(sid)) {
63
82
  try { req.logout?.(); } catch (e) {}
64
83
  if (req.session) {
65
84
  try { req.session.destroy(() => {}); } catch (e) {}
@@ -71,15 +90,12 @@ Plugin.checkSession = async function (data) {
71
90
  req.originalUrl?.startsWith('/api/');
72
91
 
73
92
  if (wantsJson) {
74
- res.status(401).json({
75
- error: 'session_terminated',
76
- message: 'Maksimum cihaz sınırına ulaşıldı.',
77
- });
78
- return; // IMPORTANT: response bitti
93
+ res.status(401).json({ error: 'session_terminated', message: 'Maksimum cihaz sınırına ulaşıldı.' });
94
+ return;
79
95
  }
80
96
 
81
97
  res.redirect('/login?error=session-conflict');
82
- return; // IMPORTANT: response bitti
98
+ return;
83
99
  }
84
100
 
85
101
  return data;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-anti-account-sharing",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Prevents account sharing by enforcing a single active session policy for desktop devices.",
5
5
  "main": "library.js",
6
6
  "keywords": [
@@ -1,71 +1,97 @@
1
1
  'use strict';
2
2
 
3
- $(document).ready(function() {
4
- // 1. URL Kontrolü (Sayfa yenileyince atıldıysa)
3
+ (function () {
4
+ function showSecurityModal() {
5
+ // Varsa eski modalları temizle
6
+ $('#antishare-modal').remove();
7
+
8
+ const modalHtml = `
9
+ <div id="antishare-modal" style="
10
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
11
+ background: rgba(0,0,0,0.92); z-index: 999999;
12
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
13
+ text-align: center; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
14
+ animation: fadeIn 0.3s ease;
15
+ ">
16
+ <div style="
17
+ background: #ffffff; padding: 40px; border-radius: 16px;
18
+ max-width: 90%; width: 400px; box-shadow: 0 10px 40px rgba(0,0,0,0.5);
19
+ position: relative; overflow: hidden;
20
+ ">
21
+ <div style="position: absolute; top:0; left:0; width:100%; height:6px; background:#d9534f;"></div>
22
+
23
+ <div style="
24
+ width: 70px; height: 70px; background: #fdf2f2; border-radius: 50%;
25
+ display: flex; align-items: center; justify-content: center; margin: 0 auto 20px;
26
+ ">
27
+ <i class="fa fa-shield" style="font-size: 32px; color: #d9534f;"></i>
28
+ </div>
29
+
30
+ <h2 style="color: #333; margin: 0 0 10px; font-size: 22px; font-weight: 700;">Oturum Sonlandırıldı</h2>
31
+
32
+ <p style="color: #666; font-size: 14px; line-height: 1.6; margin: 0 0 30px;">
33
+ Hesabınıza <strong>başka bir bilgisayardan</strong> giriş yapıldığı tespit edildi.<br><br>
34
+ Hesap güvenliğiniz için önceki cihazdaki oturum otomatik olarak kapatılmıştır.
35
+ </p>
36
+
37
+ <a href="/login" style="
38
+ display: block; width: 100%; padding: 14px 0;
39
+ background: #d9534f; color: #fff; text-decoration: none;
40
+ font-weight: 600; border-radius: 8px; font-size: 15px;
41
+ transition: background 0.2s;
42
+ " onmouseover="this.style.background='#c9302c'" onmouseout="this.style.background='#d9534f'">
43
+ TEKRAR GİRİŞ YAP
44
+ </a>
45
+
46
+ <div style="margin-top: 20px; font-size: 12px; color: #999;">
47
+ Mobil cihazlarınız bu durumdan etkilenmez.
48
+ </div>
49
+ </div>
50
+ </div>
51
+ <style>@keyframes fadeIn { from { opacity:0; transform:scale(0.95); } to { opacity:1; transform:scale(1); } }</style>
52
+ `;
53
+
54
+ $('body').append(modalHtml);
55
+ $('body').css('overflow', 'hidden');
56
+ }
57
+
58
+ function checkUrlParam() {
5
59
  const urlParams = new URLSearchParams(window.location.search);
6
60
  if (urlParams.get('error') === 'session-conflict') {
7
- showSecurityModal();
61
+ showSecurityModal();
8
62
  }
63
+ }
9
64
 
10
- // 2. AJAX Kontrolü (Sayfada gezerken atıldıysa)
11
- $(document).ajaxError(function(event, jqxhr) {
12
- if (jqxhr.status === 401 && jqxhr.responseJSON && jqxhr.responseJSON.error === 'session_terminated') {
13
- showSecurityModal();
14
- }
15
- });
65
+ // --- 1) İlk yükleme + SPA geçişleri ---
66
+ $(document).ready(checkUrlParam);
67
+ $(window).on('action:ajaxify.end', checkUrlParam);
16
68
 
17
- function showSecurityModal() {
18
- // Varsa eski modalları temizle
19
- $('#antishare-modal').remove();
69
+ // --- 2) jQuery AJAX 401 yakala ---
70
+ $(document).ajaxError(function (event, jqxhr) {
71
+ if (jqxhr && jqxhr.status === 401 && jqxhr.responseJSON && jqxhr.responseJSON.error === 'session_terminated') {
72
+ showSecurityModal();
73
+ }
74
+ });
20
75
 
21
- // Niki'den bağımsız, genel ve şık bir tasarım
22
- const modalHtml = `
23
- <div id="antishare-modal" style="
24
- position: fixed; top: 0; left: 0; width: 100%; height: 100%;
25
- background: rgba(0,0,0,0.92); z-index: 999999;
26
- display: flex; flex-direction: column; align-items: center; justify-content: center;
27
- text-align: center; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
28
- animation: fadeIn 0.3s ease;
29
- ">
30
- <div style="
31
- background: #ffffff; padding: 40px; border-radius: 16px;
32
- max-width: 90%; width: 400px; box-shadow: 0 10px 40px rgba(0,0,0,0.5);
33
- position: relative; overflow: hidden;
34
- ">
35
- <div style="position: absolute; top:0; left:0; width:100%; height:6px; background:#d9534f;"></div>
76
+ // --- 3) FETCH 401 yakala (asıl eksik buydu) ---
77
+ const _fetch = window.fetch;
78
+ window.fetch = async function (...args) {
79
+ const res = await _fetch.apply(this, args);
36
80
 
37
- <div style="
38
- width: 70px; height: 70px; background: #fdf2f2; border-radius: 50%;
39
- display: flex; align-items: center; justify-content: center; margin: 0 auto 20px;
40
- ">
41
- <i class="fa fa-shield" style="font-size: 32px; color: #d9534f;"></i>
42
- </div>
43
-
44
- <h2 style="color: #333; margin: 0 0 10px; font-size: 22px; font-weight: 700;">Oturum Sonlandırıldı</h2>
45
-
46
- <p style="color: #666; font-size: 14px; line-height: 1.6; margin: 0 0 30px;">
47
- Hesabınıza <strong>başka bir bilgisayardan</strong> giriş yapıldığı tespit edildi.<br><br>
48
- Hesap güvenliğiniz için önceki cihazdaki oturum otomatik olarak kapatılmıştır.
49
- </p>
50
-
51
- <a href="/login" style="
52
- display: block; width: 100%; padding: 14px 0;
53
- background: #d9534f; color: #fff; text-decoration: none;
54
- font-weight: 600; border-radius: 8px; font-size: 15px;
55
- transition: background 0.2s;
56
- " onmouseover="this.style.background='#c9302c'" onmouseout="this.style.background='#d9534f'">
57
- TEKRAR GİRİŞ YAP
58
- </a>
59
-
60
- <div style="margin-top: 20px; font-size: 12px; color: #999;">
61
- Mobil cihazlarınız bu durumdan etkilenmez.
62
- </div>
63
- </div>
64
- </div>
65
- <style>@keyframes fadeIn { from { opacity:0; transform:scale(0.95); } to { opacity:1; transform:scale(1); } }</style>
66
- `;
81
+ try {
82
+ if (res && res.status === 401) {
83
+ // response body json olabilir; klonlayıp okuyalım
84
+ const clone = res.clone();
85
+ const data = await clone.json().catch(() => null);
67
86
 
68
- $('body').append(modalHtml);
69
- $('body').css('overflow', 'hidden'); // Scrollu kilitle
87
+ if (data && data.error === 'session_terminated') {
88
+ showSecurityModal();
89
+ }
90
+ }
91
+ } catch (e) {
92
+ // sessiz geç
70
93
  }
71
- });
94
+
95
+ return res;
96
+ };
97
+ })();