nodebb-plugin-anti-account-sharing 1.0.4 → 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 +104 -72
- package/package.json +1 -1
- package/static/security.js +220 -79
package/library.js
CHANGED
|
@@ -1,91 +1,123 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Plugin.init = async function () {
|
|
17
|
-
console.log(`[Anti-Share] Aktif ✅ (Limit: ${MAX_DEVICES} PC)`);
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const winston = require.main.require('winston');
|
|
5
|
+
const meta = require.main.require('./src/meta');
|
|
6
|
+
|
|
7
|
+
const PLUGIN_ID = 'nodebb-plugin-anti-account-sharing';
|
|
8
|
+
|
|
9
|
+
const DEFAULTS = {
|
|
10
|
+
enabled: true,
|
|
11
|
+
enforcement: 'kick',
|
|
12
|
+
windowSeconds: 300,
|
|
13
|
+
logLevel: 'info',
|
|
14
|
+
includeAcceptLanguage: false,
|
|
15
|
+
trustProxy: true,
|
|
18
16
|
};
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (!req || !uid || !req.sessionID) return;
|
|
26
|
-
if (isMobile(req)) return;
|
|
27
|
-
|
|
28
|
-
const key = `antishare:sessionsz:${uid}`; // <-- DİKKAT: yeni key (zset)
|
|
29
|
-
const sid = req.sessionID;
|
|
18
|
+
function toBool(v) {
|
|
19
|
+
if (typeof v === 'boolean') return v;
|
|
20
|
+
if (typeof v === 'string') return ['1', 'true', 'yes', 'on'].includes(v.toLowerCase());
|
|
21
|
+
return !!v;
|
|
22
|
+
}
|
|
30
23
|
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
24
|
+
function toInt(v, dflt) {
|
|
25
|
+
const n = parseInt(v, 10);
|
|
26
|
+
return Number.isFinite(n) ? n : dflt;
|
|
27
|
+
}
|
|
36
28
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
async function getSettings() {
|
|
30
|
+
let s = {};
|
|
31
|
+
try {
|
|
32
|
+
s = await meta.settings.get(PLUGIN_ID);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
s = {};
|
|
42
35
|
}
|
|
43
|
-
};
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
37
|
+
const out = {
|
|
38
|
+
enabled: toBool(s.enabled ?? DEFAULTS.enabled),
|
|
39
|
+
enforcement: (s.enforcement || DEFAULTS.enforcement).toLowerCase(),
|
|
40
|
+
windowSeconds: toInt(s.windowSeconds ?? DEFAULTS.windowSeconds, DEFAULTS.windowSeconds),
|
|
41
|
+
logLevel: (s.logLevel || DEFAULTS.logLevel).toLowerCase(),
|
|
42
|
+
includeAcceptLanguage: toBool(s.includeAcceptLanguage ?? DEFAULTS.includeAcceptLanguage),
|
|
43
|
+
trustProxy: toBool(s.trustProxy ?? DEFAULTS.trustProxy),
|
|
44
|
+
};
|
|
49
45
|
|
|
50
|
-
if (!
|
|
51
|
-
if (!
|
|
46
|
+
if (!['kick', 'block'].includes(out.enforcement)) out.enforcement = DEFAULTS.enforcement;
|
|
47
|
+
if (!['debug', 'info', 'warn', 'error'].includes(out.logLevel)) out.logLevel = DEFAULTS.logLevel;
|
|
48
|
+
if (out.windowSeconds < 0) out.windowSeconds = DEFAULTS.windowSeconds;
|
|
52
49
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const sid = req.sessionID;
|
|
57
|
-
if (!sid) return data;
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
58
52
|
|
|
59
|
-
|
|
53
|
+
function sha1(input) {
|
|
54
|
+
return crypto.createHash('sha1').update(String(input)).digest('hex');
|
|
55
|
+
}
|
|
60
56
|
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
function getClientIp(req, trustProxy) {
|
|
58
|
+
if (trustProxy) {
|
|
59
|
+
const xff = (req.headers['x-forwarded-for'] || '').toString();
|
|
60
|
+
if (xff) return xff.split(',')[0].trim();
|
|
61
|
+
}
|
|
62
|
+
return (
|
|
63
|
+
req.ip ||
|
|
64
|
+
(req.connection && req.connection.remoteAddress) ||
|
|
65
|
+
(req.socket && req.socket.remoteAddress) ||
|
|
66
|
+
''
|
|
67
|
+
);
|
|
68
|
+
}
|
|
63
69
|
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
function getFingerprint(req, settings) {
|
|
71
|
+
const ip = getClientIp(req, settings.trustProxy);
|
|
72
|
+
const ua = (req.headers['user-agent'] || '').toString();
|
|
73
|
+
const al = (req.headers['accept-language'] || '').toString();
|
|
74
|
+
const raw = settings.includeAcceptLanguage ? `${ip}|${ua}|${al}` : `${ip}|${ua}`;
|
|
75
|
+
return sha1(raw);
|
|
76
|
+
}
|
|
66
77
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (req.session) {
|
|
71
|
-
try { req.session.destroy(() => {}); } catch (e) {}
|
|
72
|
-
}
|
|
78
|
+
function safeJson(x) {
|
|
79
|
+
try { return JSON.stringify(x); } catch (e) { return '[unjsonable]'; }
|
|
80
|
+
}
|
|
73
81
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
(req.headers.accept && req.headers.accept.includes('json')) ||
|
|
77
|
-
req.originalUrl?.startsWith('/api/');
|
|
82
|
+
function buildLogger(settings) {
|
|
83
|
+
const level = settings?.logLevel || 'info';
|
|
78
84
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
function log(lvl, msg, metaObj) {
|
|
86
|
+
const line = `[AAS] ${msg}${metaObj ? ` ${safeJson(metaObj)}` : ''}`;
|
|
87
|
+
if (winston && typeof winston[lvl] === 'function') winston[lvl](line);
|
|
88
|
+
else if (winston && typeof winston.info === 'function') winston.info(line);
|
|
83
89
|
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
if (lvl === 'error') console.error(line);
|
|
91
|
+
else if (lvl === 'warn') console.warn(line);
|
|
92
|
+
else console.log(line);
|
|
86
93
|
}
|
|
87
|
-
|
|
88
|
-
return data;
|
|
89
|
-
};
|
|
90
94
|
|
|
91
|
-
|
|
95
|
+
return {
|
|
96
|
+
debug: (msg, metaObj) => (['debug'].includes(level) ? log('info', `DEBUG: ${msg}`, metaObj) : null),
|
|
97
|
+
info: (msg, metaObj) => (['debug', 'info'].includes(level) ? log('info', msg, metaObj) : null),
|
|
98
|
+
warn: (msg, metaObj) => (['debug', 'info', 'warn'].includes(level) ? log('warn', msg, metaObj) : null),
|
|
99
|
+
error: (msg, metaObj) => log('error', msg, metaObj),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* ✅ IMPORTANT: Hook'ları burada export et
|
|
105
|
+
* security.js içinde bunlar tanımlı olacak.
|
|
106
|
+
*/
|
|
107
|
+
const Security = require('./security');
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
// helpers
|
|
111
|
+
PLUGIN_ID,
|
|
112
|
+
DEFAULTS,
|
|
113
|
+
getSettings,
|
|
114
|
+
getFingerprint,
|
|
115
|
+
getClientIp,
|
|
116
|
+
sha1,
|
|
117
|
+
buildLogger,
|
|
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
package/static/security.js
CHANGED
|
@@ -1,97 +1,238 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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');
|
|
3
|
+
const db = require.main.require('./src/database');
|
|
4
|
+
const user = require.main.require('./src/user');
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
getSettings,
|
|
8
|
+
getFingerprint,
|
|
9
|
+
buildLogger,
|
|
10
|
+
PLUGIN_ID,
|
|
11
|
+
} = require('./library');
|
|
12
|
+
|
|
13
|
+
const KEY_LAST_FP = (uid) => `uid:${uid}:aas:lastfp`;
|
|
14
|
+
const KEY_LAST_SEEN = (uid) => `uid:${uid}:aas:lastseen`; // epoch seconds
|
|
15
|
+
const KEY_LAST_IP = (uid) => `uid:${uid}:aas:lastip`;
|
|
16
|
+
const KEY_LAST_UA = (uid) => `uid:${uid}:aas:lastua`;
|
|
17
|
+
|
|
18
|
+
const SESS_SET = (uid) => `uid:${uid}:sessions`;
|
|
19
|
+
|
|
20
|
+
// NodeBB’de yaygın session obj key isimleri (kuruluma göre değişebiliyor)
|
|
21
|
+
function sessionObjectKeys(sid) {
|
|
22
|
+
return [
|
|
23
|
+
`sess:${sid}`,
|
|
24
|
+
`session:${sid}`,
|
|
25
|
+
`sessions:${sid}`,
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function getUserSessionIds(uid) {
|
|
30
|
+
try {
|
|
31
|
+
const sids = await db.getSortedSetRange(SESS_SET(uid), 0, -1);
|
|
32
|
+
return Array.isArray(sids) ? sids : [];
|
|
33
|
+
} catch (e) {
|
|
34
|
+
return [];
|
|
56
35
|
}
|
|
36
|
+
}
|
|
57
37
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
38
|
+
async function deleteSessionObjects(sid, logger) {
|
|
39
|
+
for (const k of sessionObjectKeys(sid)) {
|
|
40
|
+
try {
|
|
41
|
+
await db.delete(k);
|
|
42
|
+
logger.debug('Deleted session object', { key: k });
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// ignore
|
|
62
45
|
}
|
|
63
46
|
}
|
|
47
|
+
}
|
|
64
48
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
49
|
+
async function kickOtherSessions(uid, keepSid, logger) {
|
|
50
|
+
const all = await getUserSessionIds(uid);
|
|
51
|
+
if (!all.length) return { total: 0, removed: 0 };
|
|
68
52
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
53
|
+
let removed = 0;
|
|
54
|
+
|
|
55
|
+
for (const sid of all) {
|
|
56
|
+
if (!sid) continue;
|
|
57
|
+
if (keepSid && sid === keepSid) continue;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await db.sortedSetRemove(SESS_SET(uid), sid);
|
|
61
|
+
removed += 1;
|
|
62
|
+
logger.info('Removed session from uid sessions set', { uid, sid });
|
|
63
|
+
} catch (e) {
|
|
64
|
+
logger.warn('Failed removing session from sessions set', { uid, sid, err: e.message });
|
|
73
65
|
}
|
|
74
|
-
});
|
|
75
66
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
67
|
+
await deleteSessionObjects(sid, logger);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { total: all.length, removed };
|
|
71
|
+
}
|
|
80
72
|
|
|
73
|
+
function getCurrentSessionId(req) {
|
|
74
|
+
// Express-session genelde req.sessionID verir
|
|
75
|
+
if (req && req.sessionID) return req.sessionID;
|
|
76
|
+
|
|
77
|
+
// Cookie’den yakalamayı dene (connect.sid vb.)
|
|
78
|
+
const cookie = (req.headers && req.headers.cookie) ? String(req.headers.cookie) : '';
|
|
79
|
+
const m = cookie.match(/connect\.sid=([^;]+)/i);
|
|
80
|
+
if (m && m[1]) return decodeURIComponent(m[1]).replace(/^s:/, '').split('.')[0];
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function shouldTrigger(uid, fp, nowSec, settings) {
|
|
85
|
+
const [lastfp, lastseen] = await Promise.all([
|
|
86
|
+
db.getObjectField(KEY_LAST_FP(uid), 'v'),
|
|
87
|
+
db.getObjectField(KEY_LAST_SEEN(uid), 'v'),
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
const lastSeenNum = parseInt(lastseen, 10);
|
|
91
|
+
const lastFpStr = lastfp ? String(lastfp) : '';
|
|
92
|
+
|
|
93
|
+
if (!lastFpStr) return { trigger: false, reason: 'no_last_fp' };
|
|
94
|
+
if (lastFpStr === fp) return { trigger: false, reason: 'same_fp' };
|
|
95
|
+
|
|
96
|
+
if (!Number.isFinite(lastSeenNum)) return { trigger: true, reason: 'fp_changed_no_lastseen' };
|
|
97
|
+
|
|
98
|
+
const diff = nowSec - lastSeenNum;
|
|
99
|
+
if (diff <= settings.windowSeconds) {
|
|
100
|
+
return { trigger: true, reason: `fp_changed_within_${settings.windowSeconds}s` };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { trigger: false, reason: `fp_changed_outside_window(${diff}s)` };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function updateLast(uid, fp, req, nowSec) {
|
|
107
|
+
const ip = (req.headers && (req.headers['x-forwarded-for'] || req.ip || '')) || '';
|
|
108
|
+
const ua = (req.headers && req.headers['user-agent']) || '';
|
|
109
|
+
|
|
110
|
+
await Promise.allSettled([
|
|
111
|
+
db.setObjectField(KEY_LAST_FP(uid), 'v', fp),
|
|
112
|
+
db.setObjectField(KEY_LAST_SEEN(uid), 'v', String(nowSec)),
|
|
113
|
+
db.setObjectField(KEY_LAST_IP(uid), 'v', String(ip).slice(0, 200)),
|
|
114
|
+
db.setObjectField(KEY_LAST_UA(uid), 'v', String(ua).slice(0, 300)),
|
|
115
|
+
]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function createSecurityMiddleware(settings, logger) {
|
|
119
|
+
return async function antiAccountSharingMiddleware(req, res, next) {
|
|
81
120
|
try {
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
121
|
+
if (!settings.enabled) return next();
|
|
122
|
+
|
|
123
|
+
// NodeBB: req.uid login olmuş kullanıcı
|
|
124
|
+
const uid = parseInt(req.uid, 10);
|
|
125
|
+
if (!Number.isFinite(uid) || uid <= 0) return next();
|
|
126
|
+
|
|
127
|
+
const fp = getFingerprint(req, settings);
|
|
128
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
86
129
|
|
|
87
|
-
|
|
88
|
-
|
|
130
|
+
const check = await shouldTrigger(uid, fp, nowSec, settings);
|
|
131
|
+
|
|
132
|
+
if (check.trigger) {
|
|
133
|
+
const keepSid = getCurrentSessionId(req);
|
|
134
|
+
|
|
135
|
+
logger.warn('Account sharing suspected (fingerprint changed)', {
|
|
136
|
+
uid,
|
|
137
|
+
reason: check.reason,
|
|
138
|
+
keepSid: keepSid || '',
|
|
139
|
+
path: req.originalUrl || req.url,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (settings.enforcement === 'block') {
|
|
143
|
+
await updateLast(uid, fp, req, nowSec);
|
|
144
|
+
return res.status(403).json({
|
|
145
|
+
error: 'account-sharing-detected',
|
|
146
|
+
message: 'Bu hesap farklı cihaz/ağ üzerinden aynı anda kullanılıyor olabilir.',
|
|
147
|
+
});
|
|
89
148
|
}
|
|
149
|
+
|
|
150
|
+
// kick mode: diğer session’ları kır
|
|
151
|
+
const result = await kickOtherSessions(uid, keepSid, logger);
|
|
152
|
+
logger.info('Kick result', { uid, ...result });
|
|
90
153
|
}
|
|
154
|
+
|
|
155
|
+
// her request sonunda last’i güncelle
|
|
156
|
+
await updateLast(uid, fp, req, nowSec);
|
|
157
|
+
return next();
|
|
91
158
|
} catch (e) {
|
|
92
|
-
|
|
159
|
+
logger.error('Middleware error', { err: e.message });
|
|
160
|
+
return next();
|
|
93
161
|
}
|
|
94
|
-
|
|
95
|
-
return res;
|
|
96
162
|
};
|
|
97
|
-
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const Security = {};
|
|
166
|
+
|
|
167
|
+
Security.init = async function (params) {
|
|
168
|
+
// NodeBB hook: static:app.load
|
|
169
|
+
const { app } = params;
|
|
170
|
+
const settings = await getSettings();
|
|
171
|
+
const logger = buildLogger(settings);
|
|
172
|
+
|
|
173
|
+
logger.info('Loading security middleware', {
|
|
174
|
+
enabled: settings.enabled,
|
|
175
|
+
enforcement: settings.enforcement,
|
|
176
|
+
windowSeconds: settings.windowSeconds,
|
|
177
|
+
trustProxy: settings.trustProxy,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (!app) {
|
|
181
|
+
logger.warn('No express app passed to security.init (static:app.load?)');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Reverse proxy varsa gerçek IP için
|
|
186
|
+
try {
|
|
187
|
+
app.set('trust proxy', !!settings.trustProxy);
|
|
188
|
+
} catch (e) {}
|
|
189
|
+
|
|
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
|
+
}
|
|
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));
|
|
235
|
+
logger.info('Security middleware attached', { plugin: PLUGIN_ID });
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
module.exports = Security;
|