Fix most current issues (overlay issues, some issues with search functionality, and clear warnings functionality)

This commit is contained in:
2026-02-03 11:05:06 +01:00
parent 0bcf0829c8
commit c6204e1941
5 changed files with 322 additions and 15 deletions
+84 -5
View File
@@ -3,7 +3,7 @@ const fs = require('node:fs');
const path = require('node:path');
const axios = require('axios');
const express = require('express');
const {addWarning, getWarnings} = require('./services/warnings');
const {addWarning, getWarnings, clearWarnings} = require('./services/warnings');
const {getGuildPermissions, updateGuildPermissions} = require('./services/permissions');
const DEFAULT_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
const PERMISSION_BITS = {
@@ -129,12 +129,10 @@ const getSession = (sessionId) => {
if (!session) {
return null;
}
if (session.expiresAt <= Date.now()) {
sessionStore.delete(sessionId);
return null;
}
return session;
};
@@ -146,7 +144,6 @@ const ensureOAuthConfig = () => {
if (!DISCORD_CLIENT_ID || !DISCORD_CLIENT_SECRET || !DISCORD_REDIRECT_URI) {
return null;
}
return {
clientId: DISCORD_CLIENT_ID,
clientSecret: DISCORD_CLIENT_SECRET,
@@ -179,7 +176,6 @@ const buildAuthorizationUrl = (state) => {
state,
prompt: 'consent',
});
return `${DISCORD_OAUTH_BASE}/authorize?${params.toString()}`;
};
@@ -235,6 +231,24 @@ const fetchDiscordGuildRoles = async (guildId) => {
return response.data;
};
const fetchDiscordGuildMembersSearch = async (guildId, query) => {
if (!DISCORD_BOT_TOKEN) {
throw new Error(BOT_TOKEN_ERROR_MESSAGE);
}
const response = await axios.get(`${DISCORD_API_BASE}/guilds/${guildId}/members/search`, {
headers: {
Authorization: `Bot ${DISCORD_BOT_TOKEN}`,
},
params: {
query,
limit: 20,
},
});
return response.data;
};
const fetchDiscordBotGuilds = async () => {
if (!DISCORD_BOT_TOKEN) {
throw new Error(BOT_TOKEN_ERROR_MESSAGE);
@@ -582,6 +596,54 @@ const createDashboardApp = () => {
},
);
app.get(
'/api/guilds/:guildId/members',
ensureSession,
ensureManageableGuild,
async (req, res) => {
const query = typeof req.query.query === 'string' ? req.query.query.trim() : '';
if (query.length < 2) {
return res.json({members: []});
}
try {
const members = await fetchDiscordGuildMembersSearch(req.params.guildId, query);
const normalized = (members || [])
.map((member) => {
const user = member.user || {};
if (!user.id) {
return null;
}
return {
id: user.id,
user: {
id: user.id,
username: user.username || '',
discriminator: user.discriminator || '',
global_name: user.global_name || '',
avatar: user.avatar || '',
},
nick: member.nick || '',
};
})
.filter(Boolean);
return res.json({members: normalized});
} catch (error) {
const status = getDiscordStatusCode(error);
if (status === 403 || status === 404) {
return res.status(409).json({
error: 'BOT_NOT_IN_GUILD',
message: 'Bot not added to this guild.',
inviteUrl: buildInviteUrl(req.params.guildId),
});
}
if (error.message === BOT_TOKEN_ERROR_MESSAGE) {
return res.status(503).json({error: 'BOT_TOKEN_MISSING', message: 'Discord bot token not configured.'});
}
return sendDiscordApiError(res, error, 'Failed to search guild members');
}
},
);
app.get(
'/api/guilds/:guildId/commands',
ensureSession,
@@ -651,6 +713,23 @@ const createDashboardApp = () => {
}
},
);
app.delete(
'/api/guilds/:guildId/warnings/:userId',
ensureSession,
ensureManageableGuild,
async (req, res) => {
try {
const warnings = await clearWarnings({
guildId: req.params.guildId,
userId: req.params.userId,
});
return res.json({warnings});
} catch (error) {
return res.status(500).json({error: 'Failed to clear warnings'});
}
},
);
return app;
};
+11 -2
View File
@@ -142,12 +142,21 @@
<div class="warnings-layout">
<div class="warnings-pane">
<h2>User lookup</h2>
<p class="muted">Fetch warnings for a user ID.</p>
<p class="muted">Search members or paste an ID.</p>
<div class="field">
<label for="warningsUserSearch">Find member</label>
<input type="search" id="warningsUserSearch" placeholder="Type a username" />
</div>
<div class="member-list" id="warningsUserResults"></div>
<p class="muted" id="warningsSelectedUser">No user selected.</p>
<div class="field">
<label for="warningsUserInput">User ID</label>
<input type="text" id="warningsUserInput" placeholder="123456789" />
</div>
<button class="button" id="fetchWarningsButton">Fetch warnings</button>
<div class="button-row">
<button class="button" id="fetchWarningsButton">Fetch warnings</button>
<button class="button button--ghost" id="clearWarningsButton">Clear warnings</button>
</div>
<div class="spinner" id="warningsSpinner" hidden></div>
</div>
<div class="warnings-pane">
+159 -8
View File
@@ -9,6 +9,7 @@ const state = {
botInstalled: false,
dirty: false,
originalPermissions: null,
selectedWarningUser: null,
};
const elements = {
@@ -45,7 +46,11 @@ const elements = {
saveBar: document.getElementById('saveBar'),
botInactiveCallout: document.getElementById('botInactiveCallout'),
warningsUserInput: document.getElementById('warningsUserInput'),
warningsUserSearch: document.getElementById('warningsUserSearch'),
warningsUserResults: document.getElementById('warningsUserResults'),
warningsSelectedUser: document.getElementById('warningsSelectedUser'),
fetchWarningsButton: document.getElementById('fetchWarningsButton'),
clearWarningsButton: document.getElementById('clearWarningsButton'),
warningsList: document.getElementById('warningsList'),
warningsEmpty: document.getElementById('warningsEmpty'),
warningsSpinner: document.getElementById('warningsSpinner'),
@@ -85,7 +90,12 @@ const setLoadingDetails = (isLoading) => {
elements.settingsPanel.hidden = true;
elements.permissionsPanel.hidden = true;
elements.warningsPanel.hidden = true;
return;
}
const activeTab = document.querySelector('.tab.active')?.dataset.tab || 'settings';
elements.settingsPanel.hidden = activeTab !== 'settings';
elements.permissionsPanel.hidden = activeTab !== 'permissions';
elements.warningsPanel.hidden = activeTab !== 'warnings';
};
const setDirty = (isDirty) => {
@@ -311,13 +321,57 @@ const renderAdminUsers = () => {
});
};
const formatMemberDisplay = (member) => {
const user = member?.user || {};
const displayName = member?.nick || user.global_name || user.username || 'Unknown user';
const tag = user.discriminator && user.discriminator !== '0' ? `#${user.discriminator}` : '';
return `${displayName}${tag}`;
};
const setSelectedWarningUser = (member) => {
state.selectedWarningUser = member || null;
if (!member) {
elements.warningsSelectedUser.textContent = 'No user selected.';
return;
}
elements.warningsSelectedUser.textContent = `${formatMemberDisplay(member)} (${member.id})`;
elements.warningsUserInput.value = member.id;
elements.newWarningUserInput.value = member.id;
};
const setManualWarningUser = (userId) => {
state.selectedWarningUser = null;
if (!userId) {
elements.warningsSelectedUser.textContent = 'No user selected.';
return;
}
elements.warningsSelectedUser.textContent = `Using manual ID: ${userId}`;
};
const resetWarningSelection = () => {
setSelectedWarningUser(null);
elements.warningsUserResults.innerHTML = '';
elements.warningsUserSearch.value = '';
elements.warningsUserInput.value = '';
elements.newWarningUserInput.value = '';
elements.warningsList.innerHTML = '';
elements.warningsEmpty.hidden = true;
};
const ensureCommandEntry = (commandName) => {
if (!state.permissions.commandPermissions[commandName]) {
state.permissions.commandPermissions[commandName] = {
allowedRoleIds: [],
allowedUserIds: [],
restricted: false,
};
}
const entry = state.permissions.commandPermissions[commandName];
entry.allowedRoleIds = Array.isArray(entry.allowedRoleIds) ? entry.allowedRoleIds : [];
entry.allowedUserIds = Array.isArray(entry.allowedUserIds) ? entry.allowedUserIds : [];
if (typeof entry.restricted !== 'boolean') {
entry.restricted = entry.allowedRoleIds.length > 0 || entry.allowedUserIds.length > 0;
}
};
const renderCommands = () => {
@@ -332,6 +386,7 @@ const renderCommands = () => {
commands.forEach((commandName) => {
ensureCommandEntry(commandName);
const commandConfig = state.permissions.commandPermissions[commandName];
const isRestricted = Boolean(commandConfig.restricted);
const card = document.createElement('div');
card.className = `command-card${state.botInstalled ? '' : ' command-card--disabled'}`;
@@ -345,7 +400,7 @@ const renderCommands = () => {
toggleLabel.className = 'toggle';
const toggle = document.createElement('input');
toggle.type = 'checkbox';
toggle.checked = commandConfig.allowedRoleIds.length > 0 || commandConfig.allowedUserIds.length > 0;
toggle.checked = isRestricted;
toggleLabel.appendChild(toggle);
toggleLabel.appendChild(document.createTextNode('Restricted'));
@@ -440,7 +495,7 @@ const renderCommands = () => {
clearButton.addEventListener('click', () => {
commandConfig.allowedRoleIds = [];
commandConfig.allowedUserIds = [];
toggle.checked = false;
commandConfig.restricted = false;
setDirty(true);
renderCommands();
});
@@ -452,15 +507,15 @@ const renderCommands = () => {
toggle.addEventListener('change', () => {
setDirty(true);
commandConfig.restricted = toggle.checked;
if (!toggle.checked) {
commandConfig.allowedRoleIds = [];
commandConfig.allowedUserIds = [];
setDirty(true);
}
renderCommands();
});
const controlsDisabled = !state.botInstalled || !toggle.checked;
const controlsDisabled = !state.botInstalled || !isRestricted;
userInput.disabled = controlsDisabled;
addUserButton.disabled = controlsDisabled;
@@ -473,8 +528,8 @@ const renderCommands = () => {
setDirty(true);
},
{
disabled: !state.botInstalled || !toggle.checked,
emptyMessage: toggle.checked ? 'Select roles to allow.' : 'Allow all roles.',
disabled: !state.botInstalled || !isRestricted,
emptyMessage: isRestricted ? 'Select roles to allow.' : 'Allow all roles.',
},
);
@@ -567,6 +622,7 @@ const loadGuildDetails = async () => {
setLoadingDetails(true);
setError(null);
setDirty(false);
resetWarningSelection();
const guild = state.guilds.find((entry) => entry.id === state.selectedGuildId);
updateBotInstalled();
@@ -699,8 +755,64 @@ const switchTab = (tabName) => {
elements.warningsPanel.hidden = tabName !== 'warnings';
};
const resolveWarningUserId = (value) => {
if (value && value.trim()) {
return value.trim();
}
return state.selectedWarningUser?.id || '';
};
const renderWarningMembers = (members = []) => {
elements.warningsUserResults.innerHTML = '';
if (!members.length) {
const empty = document.createElement('span');
empty.className = 'muted';
empty.textContent = 'No members found.';
elements.warningsUserResults.appendChild(empty);
return;
}
members.forEach((member) => {
const button = document.createElement('button');
button.type = 'button';
button.className = `member-item${state.selectedWarningUser?.id === member.id ? ' active' : ''}`;
const meta = document.createElement('div');
meta.className = 'member-item__meta';
const name = document.createElement('span');
name.className = 'member-item__name';
name.textContent = formatMemberDisplay(member);
const id = document.createElement('span');
id.className = 'member-item__id';
id.textContent = member.id;
meta.appendChild(name);
meta.appendChild(id);
button.appendChild(meta);
button.addEventListener('click', () => {
setSelectedWarningUser(member);
renderWarningMembers(members);
});
elements.warningsUserResults.appendChild(button);
});
};
const searchWarningMembers = async (query) => {
if (!state.selectedGuildId || !query || query.trim().length < 2) {
elements.warningsUserResults.innerHTML = '';
return;
}
try {
const data = await fetchDashboardJson(
`/api/guilds/${state.selectedGuildId}/members?query=${encodeURIComponent(query.trim())}`,
);
const members = data.members || [];
renderWarningMembers(members);
} catch (error) {
handleFetchError(error, 'Failed to search members.');
}
};
const fetchWarnings = async () => {
const userId = elements.warningsUserInput.value.trim();
const userId = resolveWarningUserId(elements.warningsUserInput.value);
if (!userId) {
setError({message: 'Enter a user ID to fetch warnings.'});
return;
@@ -761,7 +873,7 @@ const updateWarningCount = () => {
};
const addWarning = async () => {
const userId = elements.newWarningUserInput.value.trim();
const userId = resolveWarningUserId(elements.newWarningUserInput.value);
const reason = elements.warningReasonInput.value.trim();
if (!userId || !reason) {
elements.warningStatus.textContent = 'User ID and reason are required.';
@@ -787,6 +899,29 @@ const addWarning = async () => {
}
};
const clearWarnings = async () => {
const userId = resolveWarningUserId(elements.warningsUserInput.value);
if (!userId) {
setError({message: 'Select a user to clear warnings.'});
return;
}
const proceed = window.confirm('Clear all warnings for this user?');
if (!proceed) {
return;
}
try {
await fetchDashboardJson(`/api/guilds/${state.selectedGuildId}/warnings/${userId}`, {
method: 'DELETE',
});
elements.warningsList.innerHTML = '';
elements.warningsEmpty.hidden = false;
elements.warningStatus.textContent = 'Warnings cleared.';
dashboardApi.showToast('Warnings cleared.', 'success');
} catch (error) {
handleFetchError(error, 'Failed to clear warnings.');
}
};
const refreshBotStatus = async () => {
await loadBotGuilds();
updateBotInstalled();
@@ -877,8 +1012,23 @@ const initDashboard = async () => {
elements.savePermissionsButton.addEventListener('click', savePermissions);
elements.discardChangesButton.addEventListener('click', discardChanges);
elements.fetchWarningsButton.addEventListener('click', fetchWarnings);
elements.clearWarningsButton.addEventListener('click', clearWarnings);
elements.addWarningButton.addEventListener('click', addWarning);
elements.warningReasonInput.addEventListener('input', updateWarningCount);
elements.warningsUserSearch.addEventListener(
'input',
dashboardApi.debounce((event) => {
searchWarningMembers(event.target.value);
}, 250),
);
elements.warningsUserInput.addEventListener('input', (event) => {
setManualWarningUser(event.target.value.trim());
});
elements.newWarningUserInput.addEventListener('input', (event) => {
if (event.target.value.trim()) {
setManualWarningUser(event.target.value.trim());
}
});
elements.inviteButton.addEventListener('click', openInvite);
elements.refreshBotButton.addEventListener('click', refreshBotStatus);
elements.clearAllCommands.addEventListener('click', () => {
@@ -886,6 +1036,7 @@ const initDashboard = async () => {
state.permissions.commandPermissions[commandName] = {
allowedRoleIds: [],
allowedUserIds: [],
restricted: false,
};
});
setDirty(true);
+54
View File
@@ -18,6 +18,10 @@
box-sizing: border-box;
}
[hidden] {
display: none !important;
}
body {
margin: 0;
min-height: 100vh;
@@ -532,6 +536,52 @@ textarea {
margin-top: 12px;
}
.member-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 220px;
overflow-y: auto;
padding: 10px;
border-radius: 12px;
border: 1px solid var(--border);
background: #131826;
}
.member-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: 10px;
border: 1px solid transparent;
background: var(--card);
cursor: pointer;
}
.member-item__meta {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.member-item__name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.member-item__id {
font-size: 0.8rem;
color: var(--muted);
}
.member-item.active {
border-color: var(--primary);
}
.toggle {
display: inline-flex;
align-items: center;
@@ -701,6 +751,10 @@ textarea {
z-index: 40;
}
.modal[hidden] {
display: none;
}
.modal__content {
background: var(--panel);
border-radius: 16px;
+14
View File
@@ -40,7 +40,21 @@ const getWarnings = async ({ guildId, userId }) => {
return record?.warnings ?? [];
};
const clearWarnings = async ({ guildId, userId }) => {
const collection = await getWarningsCollection();
const result = await collection.findOneAndUpdate(
{ guildId, userId },
{ $set: { warnings: [] } },
{ returnDocument: 'after', upsert: true },
);
await updateStrikes({ guildId, userId, strikes: 0 });
return result.value?.warnings ?? [];
};
module.exports = {
addWarning,
getWarnings,
clearWarnings,
};