mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
improve admin user list ui
This commit is contained in:
@@ -5,7 +5,7 @@ const io = require('../../globals/io');
|
||||
const logger = require('../../globals/logger').child('identityAdminService');
|
||||
const { getRole } = require('../roleService');
|
||||
const {
|
||||
listUsersForAdmin,
|
||||
listUserSummariesForAdmin,
|
||||
getUserForAdmin,
|
||||
addUserSignal,
|
||||
removeUserSignal,
|
||||
@@ -71,10 +71,13 @@ function ackHandler(socket, eventName, handler) {
|
||||
}
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
ackHandler(socket, 'identityAdmin:listUsers', () => ({
|
||||
users: listUsersForAdmin(),
|
||||
permissions: listRegisteredPermissions(),
|
||||
}));
|
||||
ackHandler(socket, 'identityAdmin:listUsers', ({ query, filter }) => {
|
||||
const result = listUserSummariesForAdmin({ query, filter });
|
||||
return {
|
||||
...result,
|
||||
permissions: listRegisteredPermissions(),
|
||||
};
|
||||
});
|
||||
|
||||
ackHandler(socket, 'identityAdmin:listPermissions', () => ({
|
||||
permissions: listRegisteredPermissions(),
|
||||
|
||||
@@ -19,6 +19,7 @@ const DB_PATH = resolveDataPath('identity.sqlite');
|
||||
const LEGACY_VERIFICATION_PATH = resolveDataPath('verified-users.json');
|
||||
const LEGACY_BARCODE_PATH = resolveDataPath('barcode-games.json');
|
||||
const STORE_VERSION = 4;
|
||||
const ADMIN_USER_LIST_LIMIT = 100;
|
||||
const identityEvents = new EventEmitter();
|
||||
|
||||
let db = null;
|
||||
@@ -560,6 +561,85 @@ function listUsersForAdmin() {
|
||||
}));
|
||||
}
|
||||
|
||||
function listUserSummariesForAdmin({ query = '', filter = 'all' } = {}) {
|
||||
const conn = getDb();
|
||||
const normalizedQuery = String(query || '').trim().toLowerCase().slice(0, 200);
|
||||
const normalizedFilter = ['all', 'verified', 'deterred', 'muted', 'unverified'].includes(filter)
|
||||
? filter
|
||||
: 'all';
|
||||
const conditions = [];
|
||||
const parameters = [];
|
||||
|
||||
if (normalizedFilter === 'verified') conditions.push('coalesce(user_status.verified_enabled, 0) = 1');
|
||||
if (normalizedFilter === 'deterred') conditions.push('coalesce(user_status.deterrence_enabled, 0) = 1');
|
||||
if (normalizedFilter === 'muted') conditions.push('coalesce(user_status.muted_enabled, 0) = 1');
|
||||
if (normalizedFilter === 'unverified') conditions.push('coalesce(user_status.verified_enabled, 0) = 0');
|
||||
|
||||
if (normalizedQuery) {
|
||||
const pattern = `%${normalizedQuery}%`;
|
||||
/*
|
||||
Search stays inside one bounded SQLite statement. EXISTS checks preserve
|
||||
lookup by any known identity signal without constructing every user's
|
||||
complete signal and feature-state record in JavaScript first.
|
||||
*/
|
||||
conditions.push(`(
|
||||
lower(users.id) like ?
|
||||
or exists (select 1 from user_nicknames where user_id = users.id and lower(nickname) like ?)
|
||||
or exists (select 1 from user_cookie_ids where user_id = users.id and lower(cookie_user_id) like ?)
|
||||
or exists (select 1 from user_fingerprint_ids where user_id = users.id and lower(fingerprint_id) like ?)
|
||||
or exists (select 1 from user_known_ips where user_id = users.id and lower(ip) like ?)
|
||||
or exists (select 1 from user_feature_state where user_id = users.id and lower(namespace) like ?)
|
||||
or exists (select 1 from user_permissions where user_id = users.id and lower(permission_key) like ?)
|
||||
)`);
|
||||
parameters.push(pattern, pattern, pattern, pattern, pattern, pattern, pattern);
|
||||
}
|
||||
|
||||
const where = conditions.length ? `where ${conditions.join(' and ')}` : '';
|
||||
/*
|
||||
The list needs only the newest visible signal and moderation flags. Full
|
||||
signal histories, permissions, and feature JSON remain available through
|
||||
getUserForAdmin after an administrator selects one of these summaries.
|
||||
Reading one extra row tells the UI whether it should ask for a narrower
|
||||
search without running a second full COUNT query.
|
||||
*/
|
||||
const rows = conn.prepare(`
|
||||
select
|
||||
users.id,
|
||||
users.created_at,
|
||||
users.updated_at,
|
||||
users.last_seen_at,
|
||||
coalesce(user_status.verified_enabled, 0) as verified_enabled,
|
||||
coalesce(user_status.deterrence_enabled, 0) as deterrence_enabled,
|
||||
coalesce(user_status.muted_enabled, 0) as muted_enabled,
|
||||
(select nickname from user_nicknames where user_id = users.id order by last_seen_at desc limit 1) as nickname,
|
||||
(select cookie_user_id from user_cookie_ids where user_id = users.id order by last_seen_at desc limit 1) as cookie_user_id,
|
||||
(select fingerprint_id from user_fingerprint_ids where user_id = users.id order by last_seen_at desc limit 1) as fingerprint_id
|
||||
from users
|
||||
left join user_status on user_status.user_id = users.id
|
||||
${where}
|
||||
order by coalesce(users.last_seen_at, users.updated_at, users.created_at) desc
|
||||
limit ?
|
||||
`).all(...parameters, ADMIN_USER_LIST_LIMIT + 1);
|
||||
|
||||
return {
|
||||
truncated: rows.length > ADMIN_USER_LIST_LIMIT,
|
||||
users: rows.slice(0, ADMIN_USER_LIST_LIMIT).map((row) => ({
|
||||
id: row.id,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
lastSeenAt: row.last_seen_at,
|
||||
nickname: row.nickname || null,
|
||||
cookieUserIds: row.cookie_user_id ? [row.cookie_user_id] : [],
|
||||
fingerprintIds: row.fingerprint_id ? [row.fingerprint_id] : [],
|
||||
verified: { enabled: Boolean(row.verified_enabled) },
|
||||
deterrence: {
|
||||
enabled: Boolean(row.deterrence_enabled),
|
||||
muted: Boolean(row.muted_enabled),
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function getUserForAdmin(userId) {
|
||||
const user = getUserById(userId, { includeFeatures: true });
|
||||
return user ? { ...user, featureNamespaces: Object.keys(user.features || {}).sort() } : null;
|
||||
@@ -1111,6 +1191,7 @@ module.exports = {
|
||||
attachIdentitySignals,
|
||||
getUserById,
|
||||
listUsersForAdmin,
|
||||
listUserSummariesForAdmin,
|
||||
getUserForAdmin,
|
||||
addUserSignal,
|
||||
removeUserSignal,
|
||||
|
||||
@@ -80,3 +80,36 @@ test('unknown permission keys cannot be persisted', () => {
|
||||
/Unknown user permission/,
|
||||
);
|
||||
});
|
||||
|
||||
test('administrator user summaries are bounded and searchable without loading full records', () => {
|
||||
const db = identityService.getDb();
|
||||
const insertUser = db.prepare('insert or ignore into users (id, created_at, updated_at, last_seen_at) values (?, ?, ?, ?)');
|
||||
const insertStatus = db.prepare('insert or ignore into user_status (user_id, deterrence_enabled) values (?, ?)');
|
||||
const insertNickname = db.prepare('insert or ignore into user_nicknames (user_id, nickname, first_seen_at, last_seen_at) values (?, ?, ?, ?)');
|
||||
|
||||
/*
|
||||
Seed more records than one response may contain. Direct inserts keep this
|
||||
focused test independent from browser identity generation while exercising
|
||||
the real normalized tables and the same query used by the admin socket.
|
||||
*/
|
||||
db.transaction(() => {
|
||||
for (let index = 0; index < 105; index += 1) {
|
||||
const userId = `usr_${index.toString(16).padStart(32, '0')}`;
|
||||
insertUser.run(userId, index, index, index);
|
||||
insertStatus.run(userId, index === 104 ? 1 : 0);
|
||||
insertNickname.run(userId, index === 104 ? 'Unique Search Target' : `User ${index}`, index, index);
|
||||
}
|
||||
})();
|
||||
|
||||
const recent = identityService.listUserSummariesForAdmin();
|
||||
assert.equal(recent.users.length, 100);
|
||||
assert.equal(recent.truncated, true);
|
||||
|
||||
const searched = identityService.listUserSummariesForAdmin({ query: 'unique search target' });
|
||||
assert.equal(searched.users.length, 1);
|
||||
assert.equal(searched.users[0].nickname, 'Unique Search Target');
|
||||
|
||||
const deterred = identityService.listUserSummariesForAdmin({ filter: 'deterred' });
|
||||
assert.equal(deterred.users.length, 1);
|
||||
assert.equal(deterred.users[0].deterrence.enabled, true);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user