nodebb-plugin-anti-account-sharing 1.0.0 → 1.0.2

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,76 +8,81 @@ const Plugin = {};
8
8
  // --- AYARLAR ---
9
9
  const MAX_DEVICES = 1; // Kaç bilgisayara izin verilsin?
10
10
 
11
- // Cihazın Mobil olup olmadığını anlayan fonksiyon
12
11
  function isMobile(req) {
13
- const ua = req.headers['user-agent'];
14
- return /Mobile|Android|iPhone|iPad|iPod/i.test(ua);
12
+ const ua = req?.headers?.['user-agent'] || '';
13
+ return /Mobile|Android|iPhone|iPad|iPod/i.test(ua);
15
14
  }
16
15
 
17
- Plugin.init = async function (params) {
18
- console.log(`[Anti-Share] Güvenlik Aktif (Limit: ${MAX_DEVICES} Bilgisayar) 🛡️`);
16
+ Plugin.init = async function () {
17
+ console.log(`[Anti-Share] Güvenlik Aktif (Limit: ${MAX_DEVICES} Bilgisayar) 🛡️`);
19
18
  };
20
19
 
21
- // 1. GİRİŞ YAPILDIĞINDA (Listeye Ekle)
20
+ // 1) Login olunca session'ı kaydet
22
21
  Plugin.recordSession = async function (data) {
23
- // Sadece Bilgisayar girişlerini takip ediyoruz
24
- if (!isMobile(data.req)) {
25
- if (data.uid && data.req.sessionID) {
26
- const key = `antishare:sessions:${data.uid}`;
27
-
28
- // 1. Yeni oturumu listenin SONUNA ekle
29
- await db.listAppend(key, data.req.sessionID);
30
-
31
- // 2. Listeyi kırp (Sadece son 5 taneyi tut)
32
- // (Eğer 6. kişi girerse, listenin başındaki 1. kişi silinir)
33
- await db.listTrim(key, -MAX_DEVICES, -1);
34
- }
35
- }
22
+ const req = data?.req;
23
+ const uid = data?.uid;
24
+
25
+ // req yoksa sessizce çık (bazı akışlarda gelemeyebiliyor)
26
+ if (!req || !uid || !req.sessionID) return;
27
+
28
+ // Mobil girişleri takip etmiyoruz
29
+ if (isMobile(req)) return;
30
+
31
+ const key = `antishare:sessions:${uid}`;
32
+
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);
36
36
  };
37
37
 
38
- // 2. HER SAYFA GEZİNTİSİNDE (Kontrol Et)
38
+ // 2) Her page + api isteğinde kontrol
39
39
  Plugin.checkSession = async function (data) {
40
- const req = data.req;
41
- const res = data.res;
40
+ const req = data?.req;
41
+ const res = data?.res;
42
+
43
+ if (!req || !res) return data;
44
+
45
+ // login değilse veya mobilse karışma
46
+ if (!req.uid || req.uid <= 0 || isMobile(req)) return data;
42
47
 
43
- // Giriş yapmamışsa veya Mobilden giriyorsa KARIŞMA
44
- if (!req.uid || req.uid <= 0 || isMobile(req)) {
45
- return data;
48
+ // admin ise karışma
49
+ const isAdmin = await user.isAdministrator(req.uid);
50
+ if (isAdmin) return data;
51
+
52
+ const key = `antishare:sessions:${req.uid}`;
53
+ const validSessions = await db.getListRange(key, 0, -1);
54
+
55
+ // liste boşsa (ilk login vs.) karışma
56
+ if (!Array.isArray(validSessions) || !validSessions.length) return data;
57
+
58
+ // sessionID yoksa karışma
59
+ if (!req.sessionID) return data;
60
+
61
+ if (!validSessions.includes(req.sessionID)) {
62
+ // Oturumu öldür
63
+ try { req.logout?.(); } catch (e) {}
64
+ if (req.session) {
65
+ try { req.session.destroy(() => {}); } catch (e) {}
46
66
  }
47
67
 
48
- const isAdmin = await user.isAdministrator(req.uid);
49
- if (isAdmin) return data;
50
-
51
- // --- BİLGİSAYAR KONTROLÜ ---
52
- const key = `antishare:sessions:${req.uid}`;
53
-
54
- // Veritabanındaki "Geçerli Oturum Listesi"ni çek
55
- const validSessions = await db.getListRange(key, 0, -1);
56
-
57
- // Eğer şu anki oturum ID'si, izin verilenler listesinde YOKSA -> AT!
58
- if (validSessions && !validSessions.includes(req.sessionID)) {
59
-
60
- // Oturumu Öldür
61
- req.logout();
62
- if (req.session) req.session.destroy();
63
-
64
- if (req.xhr || (req.headers.accept && req.headers.accept.indexOf('json') > -1)) {
65
- return res.status(401).json({
66
- error: 'session_terminated',
67
- message: 'Maksimum cihaz sınırına ulaşıldı.'
68
- });
69
- } else {
70
- return res.redirect('/login?error=session-conflict');
71
- }
68
+ const wantsJson =
69
+ req.xhr ||
70
+ (req.headers.accept && req.headers.accept.includes('json')) ||
71
+ req.originalUrl?.startsWith('/api/');
72
+
73
+ 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
72
79
  }
73
80
 
74
- return data;
75
- };
81
+ res.redirect('/login?error=session-conflict');
82
+ return; // IMPORTANT: response bitti
83
+ }
76
84
 
77
- // Client scriptini ekle
78
- Plugin.addScripts = async function (scripts) {
79
- scripts.push('plugins/nodebb-plugin-anti-account-sharing/static/security.js');
80
- return scripts;
85
+ return data;
81
86
  };
82
87
 
83
- module.exports = Plugin;
88
+ module.exports = Plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-anti-account-sharing",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Prevents account sharing by enforcing a single active session policy for desktop devices.",
5
5
  "main": "library.js",
6
6
  "keywords": [
package/plugin.json CHANGED
@@ -1,19 +1,18 @@
1
1
  {
2
- "id": "nodebb-plugin-niki-antishare",
3
- "name": "Niki Anti-Share Security",
4
- "description": "Bilgisayar oturumlarını tekilleştirerek hesap paylaşımını engeller (Mobil hariç).",
5
- "url": "https://forum.ieu.app",
6
- "library": "./library.js",
7
- "hooks": [
8
- { "hook": "static:app.load", "method": "init" },
9
- { "hook": "action:user.loggedIn", "method": "recordSession" },
10
- { "hook": "filter:router.page", "method": "checkSession" },
11
- { "hook": "filter:scripts.get", "method": "addScripts" }
12
- ],
13
- "scripts": [
14
- "static/security.js"
15
- ],
16
- "staticDirs": {
17
- "static": "./static"
18
- }
19
- }
2
+ "id": "nodebb-plugin-anti-account-sharing",
3
+ "name": "Anti-Share Security",
4
+ "description": "Bilgisayar oturumlarını tekilleştirerek hesap paylaşımını engeller (Mobil hariç).",
5
+ "library": "./library.js",
6
+ "hooks": [
7
+ { "hook": "static:app.load", "method": "init" },
8
+ { "hook": "action:user.loggedIn", "method": "recordSession" },
9
+ { "hook": "filter:router.page", "method": "checkSession" },
10
+ { "hook": "filter:router.api", "method": "checkSession" }
11
+ ],
12
+ "scripts": [
13
+ "static/security.js"
14
+ ],
15
+ "staticDirs": {
16
+ "static": "./static"
17
+ }
18
+ }
@@ -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
+ })();