This commit is contained in:
2026-09-13 04:05:59 +02:00
parent 90860b9279
commit 340c825019
48 changed files with 2727 additions and 532 deletions
+200 -75
View File
@@ -77,7 +77,15 @@ export default (router, tpl) => {
} else {
const reason = user[0].ban_reason || 'none';
const expires = user[0].ban_expires ? new Date(user[0].ban_expires).toISOString().replace('T', ' ').substring(0, 16) : 'never';
return fail(`You are banned! reason: ${reason} expire: ${expires}`);
if (req.headers['x-requested-with'] === 'XMLHttpRequest' || (req.headers.accept && req.headers.accept.includes('application/json'))) {
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({
success: false,
banned: true,
msg: `You are banned! reason: ${reason} expire: ${expires}`,
redirect: '/banned'
}));
}
return res.writeHead(302, { Location: '/banned' }).end();
}
}
@@ -452,6 +460,14 @@ export default (router, tpl) => {
await audit.log(req.session.id, 'ban_ip', 'ip', null, { ip, reason, duration });
// Broadcast ban event via SSE
await db.notify('bans', JSON.stringify({
ip,
ipHash,
reason: (reason || 'Banned by moderator').substring(0, 300),
expires
})).catch(() => {});
return res.json({ success: true });
} catch (err) {
return res.json({ success: false, msg: err.message });
@@ -682,6 +698,16 @@ export default (router, tpl) => {
expires,
banIps: true
});
} else {
// Broadcast ban to registered user's active SSE sessions
const userIps = await db`SELECT distinct ip FROM user_ips WHERE user_id = ${+user_id}`;
const ips = userIps.map(r => r.ip).filter(Boolean);
await db.notify('bans', JSON.stringify({
userId: +user_id,
reason: (reason || 'Violation of community rules').substring(0, 300),
expires,
ips
})).catch(() => {});
}
// Log it in audit
@@ -1109,87 +1135,179 @@ export default (router, tpl) => {
const page = Math.max(1, parseInt(req.url.qs?.page) || 1);
const limit = 50;
const offset = (page - 1) * limit;
const rawStatus = (req.url.qs?.status || req.url.qs?.filter || '').toLowerCase().trim();
const rawRole = (req.url.qs?.role || '').toLowerCase().trim();
const users = await db`
WITH filtered_users AS (
SELECT
u.id, u.login, u.user, u.email, u.created_at, u.banned, u.is_moderator, u.admin, u.activated,
uo.avatar_file, uo.display_name, uo.force_comment_display_mode, uo.comment_display_mode,
(SELECT token FROM invite_tokens WHERE used_by = u.id ORDER BY created_at DESC LIMIT 1) as reg_method
FROM "user" u
LEFT JOIN user_options uo ON uo.user_id = u.id
${q ? (exactMatch
? db`WHERE lower(u.login) = lower(${q}) OR lower(u.user) = lower(${q}) OR lower(u.email) = lower(${q})`
: db`WHERE u.login ILIKE ${'%' + lib.escapeLike(q) + '%'} OR u.user ILIKE ${'%' + lib.escapeLike(q) + '%'} OR u.email ILIKE ${'%' + lib.escapeLike(q) + '%'}`
) : db``}
),
ghost_users AS (
SELECT
NULL::int as id, i.username as login, i.username as "user", 'Legacy Account' as email,
to_timestamp(MIN(i.stamp)) as created_at, false as banned, false as is_moderator, false as admin, true as activated,
NULL::text as avatar_file, NULL::varchar as display_name, 0 as force_comment_display_mode, 0 as comment_display_mode, 'Legacy' as reg_method
const onlyLegacy = req.url.qs?.legacy === '1' || req.url.qs?.legacy === 'true' ||
req.url.qs?.legacy_only === '1' || req.url.qs?.legacy_only === 'true' ||
req.url.qs?.only_legacy === '1' || req.url.qs?.only_legacy === 'true' ||
rawStatus === 'legacy';
const status = onlyLegacy ? '' : rawStatus;
const role = onlyLegacy ? '' : rawRole;
let users;
let total;
if (onlyLegacy) {
users = await db`
WITH ghost_users AS (
SELECT
NULL::int as id, i.username as login, i.username as "user", 'Legacy Account' as email,
to_timestamp(MIN(i.stamp)) as created_at, false as banned, false as is_moderator, false as admin, true as activated,
NULL::text as avatar_file, NULL::varchar as display_name, 0 as force_comment_display_mode, 0 as comment_display_mode, 'Legacy' as reg_method
FROM items i
WHERE i.username IS NOT NULL AND i.username != ''
AND NOT EXISTS (SELECT 1 FROM "user" u WHERE u.login = i.username OR u.user = i.username)
${q ? (exactMatch
? db`AND lower(i.username) = lower(${q})`
: db`AND (i.username ILIKE ${'%' + lib.escapeLike(q) + '%'})`
) : db``}
GROUP BY i.username
),
paginated_users AS (
SELECT * FROM ghost_users
ORDER BY created_at DESC
LIMIT ${limit} OFFSET ${offset}
)
SELECT
pu.*,
EXTRACT(DAY FROM (now() - pu.created_at)) as age_days,
COALESCE(ic.upload_count, 0) as upload_count,
0::bigint as comment_count,
0::bigint as failed_attempts
FROM paginated_users pu
LEFT JOIN LATERAL (
SELECT COUNT(*) as upload_count
FROM items
WHERE (username = pu.login OR username = pu.user) AND is_deleted = false
) ic ON true
`;
const totalCountGhost = await db`
SELECT COUNT(DISTINCT i.username) as c
FROM items i
WHERE NOT EXISTS (SELECT 1 FROM "user" u WHERE u.login = i.username OR u.user = i.username)
WHERE i.username IS NOT NULL AND i.username != ''
AND NOT EXISTS (SELECT 1 FROM "user" u WHERE u.login = i.username OR u.user = i.username)
${q ? (exactMatch
? db`AND lower(i.username) = lower(${q})`
: db`AND (i.username ILIKE ${'%' + lib.escapeLike(q) + '%'})`
) : db``}
GROUP BY i.username
),
all_users AS (
SELECT * FROM filtered_users
UNION ALL
SELECT * FROM ghost_users
),
paginated_users AS (
SELECT * FROM all_users
ORDER BY created_at DESC
LIMIT ${limit} OFFSET ${offset}
)
SELECT
pu.*,
EXTRACT(DAY FROM (now() - pu.created_at)) as age_days,
COALESCE(ic.upload_count, 0) as upload_count,
COALESCE(cc.comment_count, 0) as comment_count,
COALESCE(la.failed_attempts, 0) as failed_attempts
FROM paginated_users pu
LEFT JOIN LATERAL (
SELECT COUNT(*) as upload_count
FROM items
WHERE (username = pu.login OR username = pu.user) AND is_deleted = false
) ic ON true
LEFT JOIN LATERAL (
SELECT COUNT(*) as comment_count
FROM comments
WHERE user_id = pu.id AND is_deleted = false
) cc ON pu.id IS NOT NULL
LEFT JOIN LATERAL (
SELECT COUNT(*) as failed_attempts
FROM login_attempts
WHERE username = pu.login
AND success = false
AND type = 'login'
AND attempted_at > now() - interval '10 hours'
) la ON true
`;
`;
total = parseInt(totalCountGhost[0].c);
} else {
let qCond = null;
if (q) {
if (exactMatch) {
qCond = db`(lower(u.login) = lower(${q}) OR lower(u.user) = lower(${q}) OR lower(u.email) = lower(${q}))`;
} else {
const pattern = '%' + lib.escapeLike(q) + '%';
qCond = db`(u.login ILIKE ${pattern} OR u.user ILIKE ${pattern} OR u.email ILIKE ${pattern})`;
}
}
const totalCountActual = await db`
SELECT COUNT(*) as c FROM "user" u
${q ? (exactMatch
? db`WHERE lower(u.login) = lower(${q}) OR lower(u.user) = lower(${q}) OR lower(u.email) = lower(${q})`
: db`WHERE u.login ILIKE ${'%' + lib.escapeLike(q) + '%'} OR u.user ILIKE ${'%' + lib.escapeLike(q) + '%'} OR u.email ILIKE ${'%' + lib.escapeLike(q) + '%'}`
) : db``}
`;
const totalCountGhost = await db`
SELECT COUNT(DISTINCT i.username) as c
FROM items i
WHERE NOT EXISTS (SELECT 1 FROM "user" u WHERE u.login = i.username OR u.user = i.username)
${q ? (exactMatch
? db`AND lower(i.username) = lower(${q})`
: db`AND (i.username ILIKE ${'%' + lib.escapeLike(q) + '%'})`
) : db``}
`;
const total = parseInt(totalCountActual[0].c) + parseInt(totalCountGhost[0].c);
let statusCond = null;
if (status === 'banned') {
statusCond = db`u.banned = true`;
} else if (status === 'active') {
statusCond = db`(u.activated = true AND u.banned = false)`;
} else if (status === 'pending') {
statusCond = db`(u.activated = false AND u.banned = false)`;
}
let roleCond = null;
if (role === 'staff' || status === 'staff') {
roleCond = db`(u.admin = true OR u.is_moderator = true)`;
} else if (role === 'admin') {
roleCond = db`u.admin = true`;
} else if (role === 'mod') {
roleCond = db`(u.is_moderator = true AND u.admin = false)`;
} else if (role === 'user') {
roleCond = db`(u.admin = false AND u.is_moderator = false)`;
}
users = await db`
WITH filtered_users AS (
SELECT
u.id, u.login, u.user, u.email, u.created_at, u.banned, u.is_moderator, u.admin, u.activated,
uo.avatar_file, uo.display_name, uo.force_comment_display_mode, uo.comment_display_mode,
(SELECT token FROM invite_tokens WHERE used_by = u.id ORDER BY created_at DESC LIMIT 1) as reg_method
FROM "user" u
LEFT JOIN user_options uo ON uo.user_id = u.id
WHERE true
${qCond ? db`AND ${qCond}` : db``}
${statusCond ? db`AND ${statusCond}` : db``}
${roleCond ? db`AND ${roleCond}` : db``}
),
paginated_users AS (
SELECT * FROM filtered_users
ORDER BY created_at DESC
LIMIT ${limit} OFFSET ${offset}
)
SELECT
pu.*,
EXTRACT(DAY FROM (now() - pu.created_at)) as age_days,
COALESCE(ic.upload_count, 0) as upload_count,
COALESCE(cc.comment_count, 0) as comment_count,
COALESCE(la.failed_attempts, 0) as failed_attempts
FROM paginated_users pu
LEFT JOIN LATERAL (
SELECT COUNT(*) as upload_count
FROM items
WHERE (username = pu.login OR username = pu.user) AND is_deleted = false
) ic ON true
LEFT JOIN LATERAL (
SELECT COUNT(*) as comment_count
FROM comments
WHERE user_id = pu.id AND is_deleted = false
) cc ON pu.id IS NOT NULL
LEFT JOIN LATERAL (
SELECT COUNT(*) as failed_attempts
FROM login_attempts
WHERE username = pu.login
AND success = false
AND type = 'login'
AND attempted_at > now() - interval '10 hours'
) la ON true
`;
const totalCountActual = await db`
SELECT COUNT(*) as c FROM "user" u
WHERE true
${qCond ? db`AND ${qCond}` : db``}
${statusCond ? db`AND ${statusCond}` : db``}
${roleCond ? db`AND ${roleCond}` : db``}
`;
total = parseInt(totalCountActual[0].c);
}
let totalLabel = 'registered members';
let emptyMsg = 'No users matched your search.';
if (onlyLegacy) {
totalLabel = 'legacy accounts';
emptyMsg = 'No legacy users matched your search.';
} else if (status === 'banned') {
totalLabel = 'banned members';
emptyMsg = 'No banned users found.';
} else if (status === 'pending') {
totalLabel = 'pending members';
emptyMsg = 'No pending users found.';
} else if (status === 'active') {
totalLabel = 'active members';
emptyMsg = 'No active users found.';
} else if (role === 'staff' || status === 'staff') {
totalLabel = 'staff members';
emptyMsg = 'No staff members found.';
} else if (role === 'admin') {
totalLabel = 'admin members';
emptyMsg = 'No admin users found.';
} else if (role === 'mod') {
totalLabel = 'moderator members';
emptyMsg = 'No moderator users found.';
} else if (role === 'user') {
totalLabel = 'regular users';
emptyMsg = 'No regular users found.';
}
const data = {
session: req.session,
@@ -1198,6 +1316,11 @@ export default (router, tpl) => {
page,
total,
hasMore: users.length === limit,
onlyLegacy,
status,
role,
totalLabel,
emptyMsg,
totals: await lib.countf0cks(),
log_user_ips: getLogUserIps(),
tmp: null
@@ -1205,6 +1328,8 @@ export default (router, tpl) => {
if (req.headers['x-requested-with'] === 'XMLHttpRequest') {
res.setHeader('X-Total-Count', total.toString());
res.setHeader('X-Total-Label', totalLabel);
res.setHeader('X-Empty-Msg', emptyMsg);
res.setHeader('X-Has-More', (users.length === limit).toString());
return res.reply({
body: tpl.render("admin/users_list", data, req)