This commit is contained in:
2026-09-12 21:55:40 +02:00
parent 53055ea5c6
commit 90860b9279
24 changed files with 1958 additions and 45 deletions
+209
View File
@@ -323,6 +323,196 @@ export default (router, tpl) => {
});
});
router.get(/^\/admin\/bans(\/)?$/, lib.modAuth, async (req, res) => {
const bannedFingerprints = await db`
select bf.*,
u.login as anon_login, u.user as anon_user,
admin.user as banned_by_user
from banned_fingerprints bf
left join anon_identities ai on ai.fingerprint = bf.fingerprint
left join "user" u on u.id = ai.user_id
left join "user" admin on admin.id = bf.banned_by
order by bf.created_at desc
`;
const bannedHardware = await db`
select bh.*,
admin.user as banned_by_user
from banned_hardware_fingerprints bh
left join "user" admin on admin.id = bh.banned_by
order by bh.created_at desc
`;
const bannedIps = await db`
select bi.*,
admin.user as banned_by_user
from banned_ips bi
left join "user" admin on admin.id = bi.banned_by
order by bi.created_at desc
`;
const anonIdentities = await db`
select ai.*,
u.id as user_id, u.login, u.user, u.banned as is_banned, u.ban_reason,
bf.id as is_fp_banned,
bh.id as is_hw_banned
from anon_identities ai
left join "user" u on u.id = ai.user_id
left join banned_fingerprints bf on bf.fingerprint = ai.fingerprint
left join banned_hardware_fingerprints bh on bh.hw_fingerprint = ai.hw_fingerprint
order by ai.last_seen desc
limit 100
`;
const recentActivity = await db`
select al.*,
u.login, u.user
from anon_activity_log al
left join "user" u on u.id = al.user_id
order by al.created_at desc
limit 100
`;
res.reply({
body: tpl.render("admin/bans", {
session: req.session,
csrf_token: req.session ? req.session.csrf_token : '',
bannedFingerprints,
bannedHardware,
bannedIps,
anonIdentities,
recentActivity,
tmp: null
}, req)
});
});
router.post(/^\/api\/v2\/admin\/bans\/fingerprint\/ban\/?$/, lib.modAuth, async (req, res) => {
try {
const { fingerprint, reason, duration, ban_ips, user_id } = req.post || {};
if (!fingerprint) throw new Error('Missing fingerprint');
const expires = duration === 'permanent' || !duration ? null : new Date(Date.now() + parseInt(duration, 10) * 3600000);
const result = await security.banAnonymousUser({
userId: user_id ? +user_id : null,
fingerprint,
bannedBy: req.session.id,
reason: reason || 'Banned by moderator',
expires,
banIps: ban_ips !== false
});
await audit.log(req.session.id, 'ban_fingerprint', 'fingerprint', null, { fingerprint, reason, duration });
return res.json({ success: true, result });
} catch (err) {
return res.json({ success: false, msg: err.message });
}
});
router.post(/^\/api\/v2\/admin\/bans\/fingerprint\/unban\/?$/, lib.modAuth, async (req, res) => {
try {
const { fingerprint } = req.post || {};
if (!fingerprint) throw new Error('Missing fingerprint');
await db`DELETE FROM banned_fingerprints WHERE fingerprint = ${fingerprint}`;
// Also unban any shadow user associated with this fingerprint
const ident = await db`SELECT user_id FROM anon_identities WHERE fingerprint = ${fingerprint}`;
if (ident.length > 0) {
await db`UPDATE "user" SET banned = false, ban_reason = null, ban_expires = null WHERE id = ${ident[0].user_id}`;
}
await audit.log(req.session.id, 'unban_fingerprint', 'fingerprint', null, { fingerprint });
return res.json({ success: true });
} catch (err) {
return res.json({ success: false, msg: err.message });
}
});
router.post(/^\/api\/v2\/admin\/bans\/ip\/ban\/?$/, lib.modAuth, async (req, res) => {
try {
const { ip, reason, duration } = req.post || {};
if (!ip) throw new Error('Missing IP address');
const expires = duration === 'permanent' || !duration ? null : new Date(Date.now() + parseInt(duration, 10) * 3600000);
const ipHash = security.hashIP(ip);
await db`
INSERT INTO banned_ips (ip, ip_hash, banned_by, reason, expires_at)
VALUES (${ip}, ${ipHash}, ${req.session.id}, ${reason || 'Banned by moderator'}, ${expires})
ON CONFLICT (ip) DO UPDATE
SET reason = EXCLUDED.reason,
expires_at = EXCLUDED.expires_at,
banned_by = EXCLUDED.banned_by,
ip_hash = EXCLUDED.ip_hash
`;
await audit.log(req.session.id, 'ban_ip', 'ip', null, { ip, reason, duration });
return res.json({ success: true });
} catch (err) {
return res.json({ success: false, msg: err.message });
}
});
router.post(/^\/api\/v2\/admin\/bans\/ip\/unban\/?$/, lib.modAuth, async (req, res) => {
try {
const { ip } = req.post || {};
if (!ip) throw new Error('Missing IP');
await db`DELETE FROM banned_ips WHERE ip = ${ip} OR ip_hash = ${ip}`;
await audit.log(req.session.id, 'unban_ip', 'ip', null, { ip });
return res.json({ success: true });
} catch (err) {
return res.json({ success: false, msg: err.message });
}
});
router.post(/^\/api\/v2\/admin\/bans\/hardware\/ban\/?$/, lib.modAuth, async (req, res) => {
try {
const { hw_fingerprint, reason, duration, ban_ips, user_id } = req.post || {};
if (!hw_fingerprint) throw new Error('Missing hardware fingerprint');
const expires = duration === 'permanent' || !duration ? null : new Date(Date.now() + parseInt(duration, 10) * 3600000);
const result = await security.banAnonymousUser({
userId: user_id ? +user_id : null,
hwFingerprint: hw_fingerprint,
bannedBy: req.session.id,
reason: reason || 'Banned by moderator',
expires,
banIps: ban_ips !== false,
banHardware: true
});
await audit.log(req.session.id, 'ban_hardware', 'hardware', null, { hw_fingerprint, reason, duration });
return res.json({ success: true, result });
} catch (err) {
return res.json({ success: false, msg: err.message });
}
});
router.post(/^\/api\/v2\/admin\/bans\/hardware\/unban\/?$/, lib.modAuth, async (req, res) => {
try {
const { hw_fingerprint } = req.post || {};
if (!hw_fingerprint) throw new Error('Missing hardware fingerprint');
await db`DELETE FROM banned_hardware_fingerprints WHERE hw_fingerprint = ${hw_fingerprint}`;
await audit.log(req.session.id, 'unban_hardware', 'hardware', null, { hw_fingerprint });
return res.json({ success: true });
} catch (err) {
return res.json({ success: false, msg: err.message });
}
});
router.get(/^\/admin\/user\/(?<userId>\d+)\/ips(\/)?$/, lib.auth, async (req, res) => {
const userId = +req.params.userId;
const user = await db`select "user", login from "user" where id = ${userId} limit 1`;
@@ -481,6 +671,19 @@ export default (router, tpl) => {
where id = ${+user_id}
`;
// If this is an anonymous identity, cascade the ban to fingerprint and IPs
const anonIdent = await db`SELECT fingerprint FROM anon_identities WHERE user_id = ${+user_id} LIMIT 1`;
if (anonIdent.length > 0 || (targetUser[0].login && targetUser[0].login.startsWith('anon_'))) {
await security.banAnonymousUser({
userId: +user_id,
fingerprint: anonIdent[0]?.fingerprint || null,
bannedBy: req.session.id,
reason,
expires,
banIps: true
});
}
// Log it in audit
await audit.log(req.session.id, 'ban_user', 'user', +user_id, { reason, duration, target_user: targetUser[0].user });
@@ -516,6 +719,12 @@ export default (router, tpl) => {
where id = ${+user_id}
`;
// Clean up any banned fingerprint for this identity
const anonIdent = await db`SELECT fingerprint FROM anon_identities WHERE user_id = ${+user_id} LIMIT 1`;
if (anonIdent.length > 0) {
await db`DELETE FROM banned_fingerprints WHERE fingerprint = ${anonIdent[0].fingerprint}`;
}
// Log it in audit
await audit.log(req.session.id, 'unban_user', 'user', +user_id);