diff --git a/commands/db/balance.js b/commands/db/balance.js index f6d352e..e084aa5 100644 --- a/commands/db/balance.js +++ b/commands/db/balance.js @@ -15,11 +15,9 @@ module.exports = { const dbClient = await connectToDatabase(); const db = dbClient.db('discord'); const collection = db.collection('currency'); - const target = interaction.options.getUser('user') ?? interaction.user; const targetData = await collection.findOne({ userID: target.id }); const balance = targetData?.balance ?? 0; - return interaction.reply({ content: `${target.tag} has **${balance}** coins.`, }); diff --git a/commands/db/earn.js b/commands/db/earn.js index b2ff060..25b3618 100644 --- a/commands/db/earn.js +++ b/commands/db/earn.js @@ -9,12 +9,10 @@ const formatDuration = (ms) => { const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; - const parts = []; if (hours) parts.push(`${hours}h`); if (minutes) parts.push(`${minutes}m`); if (!hours && !minutes) parts.push(`${seconds}s`); - return parts.join(' '); }; @@ -28,11 +26,9 @@ module.exports = { const db = dbClient.db('discord'); const collection = db.collection('currency'); const now = new Date(); - const userId = interaction.user.id; const userData = await collection.findOne({ userID: userId }); const lastClaim = userData?.lastClaimedAt ? new Date(userData.lastClaimedAt) : null; - if (lastClaim) { const elapsed = now - lastClaim; if (elapsed < COOLDOWN_MS) { @@ -43,7 +39,6 @@ module.exports = { }); } } - await collection.updateOne( { userID: userId }, { @@ -52,7 +47,6 @@ module.exports = { }, { upsert: true } ); - return interaction.reply({ content: `You earned **${REWARD_AMOUNT}** coins! Come back tomorrow for more.`, }); diff --git a/commands/db/give.js b/commands/db/give.js index f8dca2e..2eab92e 100644 --- a/commands/db/give.js +++ b/commands/db/give.js @@ -22,23 +22,19 @@ module.exports = { const dbClient = await connectToDatabase(); const target = interaction.options.getUser('target'); const amount = interaction.options.getInteger('amount'); - if (target.bot) { return interaction.reply({ content: 'You cannot grant coins to bot accounts.', ephemeral: true, }); } - const db = dbClient.db('discord'); const collection = db.collection('currency'); - await collection.updateOne( { userID: target.id }, { $inc: { balance: amount } }, { upsert: true } ); - return interaction.reply({ content: `You granted **${amount}** coins to ${target.tag}.`, }); diff --git a/commands/db/test.js b/commands/db/test.js index 937f368..ad584a2 100644 --- a/commands/db/test.js +++ b/commands/db/test.js @@ -1,4 +1,3 @@ -// exampleCommand.js const { SlashCommandBuilder } = require('discord.js'); const { connectToDatabase } = require('../../db.js'); @@ -15,14 +14,9 @@ module.exports = { async execute(interaction) { const dbClient = await connectToDatabase(); const message = interaction.options.getString('message'); - - // Choose a DB + Collection const db = dbClient.db('discord'); const collection = db.collection('test'); - - // Example: Insert something await collection.insertOne({ userId: interaction.user.id, message: message }); - await interaction.reply('Successfully inserted your data into MongoDB.'); }, }; diff --git a/commands/moderation/kick.js b/commands/moderation/kick.js index e10c0c4..59fd524 100644 --- a/commands/moderation/kick.js +++ b/commands/moderation/kick.js @@ -30,7 +30,6 @@ module.exports = { ephemeral: true, }); } - await member.kick(reason); await recordModerationAction({ guildId: interaction.guildId, @@ -40,7 +39,6 @@ module.exports = { reason, metadata: {}, }); - await interaction.reply(`Kicked ${user.tag} for: ${reason}`); } catch (error) { console.error(error); diff --git a/dashboardServer.js b/dashboardServer.js index 401b816..c094d01 100644 --- a/dashboardServer.js +++ b/dashboardServer.js @@ -1,13 +1,10 @@ const crypto = require('node:crypto'); 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 { getGuildPermissions, updateGuildPermissions } = require('./services/permissions'); - +const {addWarning, getWarnings} = require('./services/warnings'); +const {getGuildPermissions, updateGuildPermissions} = require('./services/permissions'); const DEFAULT_SESSION_TTL_MS = 24 * 60 * 60 * 1000; const PERMISSION_BITS = { ADMINISTRATOR: 0x8n, @@ -50,6 +47,14 @@ const SESSION_TTL_MS = Number.parseInt( 10, ); +console.log('[oauth config]', { + DISCORD_CLIENT_ID_present: Boolean(DISCORD_CLIENT_ID && String(DISCORD_CLIENT_ID).trim()), + DISCORD_CLIENT_SECRET_present: Boolean(DISCORD_CLIENT_SECRET && String(DISCORD_CLIENT_SECRET).trim()), + DISCORD_REDIRECT_URI: DISCORD_REDIRECT_URI || null, + configFilePath: path.join(__dirname, 'config.json'), + configFileExists: fs.existsSync(path.join(__dirname, 'config.json')), +}); + const DISCORD_API_BASE = 'https://discord.com/api'; const sessionStore = new Map(); @@ -91,7 +96,7 @@ const setCookie = (res, name, value, options = {}) => { }; const clearCookie = (res, name) => { - setCookie(res, name, '', { maxAge: 0 }); + setCookie(res, name, '', {maxAge: 0}); }; const createSession = (payload) => { @@ -103,7 +108,7 @@ const createSession = (payload) => { expiresAt, }); - return { sessionId, expiresAt }; + return {sessionId, expiresAt}; }; const getSession = (sessionId) => { @@ -225,12 +230,12 @@ const ensureSession = (req, res, next) => { const cookies = parseCookies(req.headers.cookie); const sessionId = cookies.dashboard_session; if (!sessionId) { - return res.status(401).json({ error: 'Unauthorized' }); + return res.status(401).json({error: 'Unauthorized'}); } const session = getSession(sessionId); if (!session) { - return res.status(401).json({ error: 'Unauthorized' }); + return res.status(401).json({error: 'Unauthorized'}); } req.session = session; @@ -243,16 +248,16 @@ const ensureManageableGuild = async (req, res, next) => { const guilds = await fetchDiscordUserGuilds(req.session.accessToken); const guild = guilds.find((entry) => entry.id === req.params.guildId); if (!guild) { - return res.status(404).json({ error: 'Guild not found' }); + return res.status(404).json({error: 'Guild not found'}); } if (!canManageGuild(guild)) { - return res.status(403).json({ error: 'Forbidden' }); + return res.status(403).json({error: 'Forbidden'}); } req.guild = guild; return next(); } catch (error) { - return res.status(502).json({ error: 'Failed to fetch guilds' }); + return res.status(502).json({error: 'Failed to fetch guilds'}); } }; @@ -260,10 +265,11 @@ const createDashboardApp = () => { const app = express(); app.use(express.json()); + app.use(express.static(path.join(__dirname, 'public'))); app.get('/auth/discord', (req, res) => { if (!ensureOAuthConfig()) { - return res.status(500).json({ error: 'Missing Discord OAuth configuration' }); + return res.status(500).json({error: 'Missing Discord OAuth configuration'}); } const state = crypto.randomBytes(16).toString('hex'); @@ -279,26 +285,25 @@ const createDashboardApp = () => { app.get('/auth/discord/callback', async (req, res) => { if (!ensureOAuthConfig()) { - return res.status(500).json({ error: 'Missing Discord OAuth configuration' }); + return res.status(500).json({error: 'Missing Discord OAuth configuration'}); } - const { code, state } = req.query; + const {code, state} = req.query; const cookies = parseCookies(req.headers.cookie); if (!code || !state || cookies.oauth_state !== state) { - return res.status(400).json({ error: 'Invalid OAuth state' }); + return res.status(400).json({error: 'Invalid OAuth state'}); } try { const tokenData = await fetchDiscordAccessToken(code); const user = await fetchDiscordUser(tokenData.access_token); - const { sessionId, expiresAt } = createSession({ + const {sessionId, expiresAt} = createSession({ accessToken: tokenData.access_token, refreshToken: tokenData.refresh_token, tokenType: tokenData.token_type, scope: tokenData.scope, user, - expiresAt, }); setCookie(res, 'dashboard_session', sessionId, { @@ -313,9 +318,22 @@ const createDashboardApp = () => { return res.redirect(DASHBOARD_CLIENT_URL); } - return res.status(200).json({ user }); + if (req.accepts('html')) { + return res.redirect('/'); + } + + return res.status(200).json({user}); } catch (error) { - return res.status(502).json({ error: 'Failed to authenticate with Discord' }); + console.error('Discord OAuth failed', { + message: error.message, + status: error.response?.status, + data: error.response?.data, + }); + + return res.status(502).json({ + error: 'Failed to authenticate with Discord', + details: error.response?.data || error.message, + }); } }); @@ -329,9 +347,9 @@ const createDashboardApp = () => { try { const guilds = await fetchDiscordUserGuilds(req.session.accessToken); const manageableGuilds = guilds.filter(canManageGuild); - return res.json({ guilds: manageableGuilds }); + return res.json({guilds: manageableGuilds}); } catch (error) { - return res.status(502).json({ error: 'Failed to fetch guilds' }); + return res.status(502).json({error: 'Failed to fetch guilds'}); } }); @@ -342,9 +360,9 @@ const createDashboardApp = () => { async (req, res) => { try { const permissions = await getGuildPermissions(req.params.guildId); - return res.json({ permissions }); + return res.json({permissions}); } catch (error) { - return res.status(500).json({ error: 'Failed to fetch permissions' }); + return res.status(500).json({error: 'Failed to fetch permissions'}); } }, ); @@ -359,9 +377,9 @@ const createDashboardApp = () => { guildId: req.params.guildId, permissions: req.body || {}, }); - return res.json({ permissions }); + return res.json({permissions}); } catch (error) { - return res.status(500).json({ error: 'Failed to update permissions' }); + return res.status(500).json({error: 'Failed to update permissions'}); } }, ); @@ -373,9 +391,9 @@ const createDashboardApp = () => { async (req, res) => { try { const roles = await fetchDiscordGuildRoles(req.params.guildId); - return res.json({ roles }); + return res.json({roles}); } catch (error) { - return res.status(500).json({ error: 'Failed to fetch roles' }); + return res.status(500).json({error: 'Failed to fetch roles'}); } }, ); @@ -390,9 +408,9 @@ const createDashboardApp = () => { guildId: req.params.guildId, userId: req.params.userId, }); - return res.json({ warnings }); + return res.json({warnings}); } catch (error) { - return res.status(500).json({ error: 'Failed to fetch warnings' }); + return res.status(500).json({error: 'Failed to fetch warnings'}); } }, ); @@ -402,11 +420,10 @@ const createDashboardApp = () => { ensureSession, ensureManageableGuild, async (req, res) => { - const { reason } = req.body || {}; + const {reason} = req.body || {}; if (!reason) { - return res.status(400).json({ error: 'Reason is required' }); + return res.status(400).json({error: 'Reason is required'}); } - try { const result = await addWarning({ guildId: req.params.guildId, @@ -416,11 +433,10 @@ const createDashboardApp = () => { }); return res.status(201).json(result); } catch (error) { - return res.status(500).json({ error: 'Failed to add warning' }); + return res.status(500).json({error: 'Failed to add warning'}); } }, ); - return app; }; diff --git a/db.js b/db.js index d2a0598..1ad12b4 100644 --- a/db.js +++ b/db.js @@ -1,11 +1,10 @@ -const { MongoClient, ServerApiVersion } = require('mongodb'); -const { mongodb } = require("./config.json"); +const {MongoClient, ServerApiVersion} = require('mongodb'); +const {mongodb} = require("./config.json"); let dbClient; async function connectToDatabase() { if (!dbClient) { - // Create a new MongoClient only once dbClient = new MongoClient(mongodb, { serverApi: { version: ServerApiVersion.v1, @@ -13,15 +12,11 @@ async function connectToDatabase() { deprecationErrors: true, } }); - await dbClient.connect(); console.log("Connected to MongoDB!"); - - await dbClient.db("admin").command({ ping: 1 }); + await dbClient.db("admin").command({ping: 1}); console.log("Pinged your deployment. You successfully connected to MongoDB!"); } - - // Return the existing connection if already connected return dbClient; } diff --git a/events/interactionsCreate.js b/events/interactionsCreate.js index 252f394..6e9e46c 100644 --- a/events/interactionsCreate.js +++ b/events/interactionsCreate.js @@ -1,18 +1,15 @@ -const { Events, MessageFlags } = require('discord.js'); -const { isCommandAllowed } = require('../services/permissions'); +const {Events, MessageFlags} = require('discord.js'); +const {isCommandAllowed} = require('../services/permissions'); module.exports = { name: Events.InteractionCreate, async execute(interaction) { if (!interaction.isChatInputCommand()) return; - const command = interaction.client.commands.get(interaction.commandName); - if (!command) { console.error(`No command matching ${interaction.commandName} was found.`); return; } - try { const allowed = await isCommandAllowed({ guildId: interaction.guildId, @@ -21,7 +18,6 @@ module.exports = { commandName: interaction.commandName, permissionGroup: command.permissionGroup, }); - if (!allowed) { await interaction.reply({ content: 'You do not have permission to use this command.', @@ -29,14 +25,19 @@ module.exports = { }); return; } - await command.execute(interaction); } catch (error) { console.error(error); if (interaction.replied || interaction.deferred) { - await interaction.followUp({ content: 'There was an error while executing this command!', flags: MessageFlags.Ephemeral }); + await interaction.followUp({ + content: 'There was an error while executing this command!', + flags: MessageFlags.Ephemeral + }); } else { - await interaction.reply({ content: 'There was an error while executing this command!', flags: MessageFlags.Ephemeral }); + await interaction.reply({ + content: 'There was an error while executing this command!', + flags: MessageFlags.Ephemeral + }); } } }, diff --git a/events/tempban.js b/events/tempban.js index 941412e..7bff351 100644 --- a/events/tempban.js +++ b/events/tempban.js @@ -10,8 +10,6 @@ module.exports = { const tempBanCollection = db.collection('tempban'); const scheduledBanIds = new Set(); let pollingInterval = null; - - // 2) Define a function to schedule unbans async function scheduleUnban(banData) { const banId = banData?._id?.toString(); if (banId && scheduledBanIds.has(banId)) { @@ -54,7 +52,6 @@ module.exports = { if (pollingInterval) { return; } - console.warn('Change streams unavailable; falling back to polling for temp bans.'); pollingInterval = setInterval(() => { loadAndScheduleBans().catch((err) => { console.error('Error polling temp bans:', err); diff --git a/index.js b/index.js index 87f3086..e5dd771 100644 --- a/index.js +++ b/index.js @@ -3,13 +3,12 @@ const { Client, Collection, GatewayIntentBits } = require('discord.js'); const { token } = require('./config.json'); const { loadCommandModules } = require('./loaders/commands'); const { loadEventModules } = require('./loaders/events'); - const client = new Client({ intents: [GatewayIntentBits.Guilds] }); - -// Retrieves commands from the command folder client.commands = new Collection(); const commandsPath = path.join(__dirname, 'commands'); const { commands, invalidCommands } = loadCommandModules(commandsPath); +const eventsPath = path.join(__dirname, 'events'); +const eventModules = loadEventModules(eventsPath); for (const command of commands) { client.commands.set(command.data.name, command); @@ -19,10 +18,6 @@ for (const filePath of invalidCommands) { console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`); } -// Retrieves events from the events folder -const eventsPath = path.join(__dirname, 'events'); -const eventModules = loadEventModules(eventsPath); - for (const { event } of eventModules) { if (event.once) { client.once(event.name, (...args) => event.execute(...args)); diff --git a/loaders/events.js b/loaders/events.js index e1668fc..c91abd9 100644 --- a/loaders/events.js +++ b/loaders/events.js @@ -11,7 +11,6 @@ const getEventFilePaths = (eventsPath) => const loadEventModules = (eventsPath) => { const eventFiles = getEventFilePaths(eventsPath); - return eventFiles.map((filePath) => ({ filePath, event: require(filePath), diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..3f50efa --- /dev/null +++ b/public/app.js @@ -0,0 +1,223 @@ +const state = { + guilds: [], + selectedGuildId: null, +}; + +const elements = { + loginButton: document.getElementById('loginButton'), + logoutButton: document.getElementById('logoutButton'), + sessionStatus: document.getElementById('sessionStatus'), + guildCard: document.getElementById('guildCard'), + warningsCard: document.getElementById('warningsCard'), + guildSelect: document.getElementById('guildSelect'), + permissionsEditor: document.getElementById('permissionsEditor'), + updatePermissionsButton: document.getElementById('updatePermissionsButton'), + rolesList: document.getElementById('rolesList'), + warningUserId: document.getElementById('warningUserId'), + loadWarningsButton: document.getElementById('loadWarningsButton'), + warningsList: document.getElementById('warningsList'), + newWarningUserId: document.getElementById('newWarningUserId'), + warningReason: document.getElementById('warningReason'), + addWarningButton: document.getElementById('addWarningButton'), + warningResult: document.getElementById('warningResult'), +}; + +const setStatus = (message, isAuthed) => { + elements.sessionStatus.textContent = message; + elements.logoutButton.hidden = !isAuthed; + elements.guildCard.hidden = !isAuthed; + elements.warningsCard.hidden = !isAuthed; +}; + +const fetchJson = async (url, options) => { + const response = await fetch(url, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + ...options, + }); + + if (!response.ok) { + const body = await response.json().catch(() => ({})); + const message = body.error || `Request failed with status ${response.status}`; + throw new Error(message); + } + + return response.json().catch(() => ({})); +}; + +const loadGuilds = async () => { + const data = await fetchJson('/api/guilds'); + state.guilds = data.guilds || []; + elements.guildSelect.innerHTML = ''; + + state.guilds.forEach((guild) => { + const option = document.createElement('option'); + option.value = guild.id; + option.textContent = guild.name; + elements.guildSelect.appendChild(option); + }); + + if (state.guilds.length > 0) { + state.selectedGuildId = state.guilds[0].id; + elements.guildSelect.value = state.selectedGuildId; + await loadGuildDetails(); + } +}; + +const renderRoles = (roles) => { + elements.rolesList.innerHTML = ''; + roles.forEach((role) => { + const item = document.createElement('li'); + item.textContent = `${role.name} (${role.id})`; + elements.rolesList.appendChild(item); + }); + + if (roles.length === 0) { + const item = document.createElement('li'); + item.textContent = 'No roles returned.'; + elements.rolesList.appendChild(item); + } +}; + +const loadGuildDetails = async () => { + if (!state.selectedGuildId) { + return; + } + + const [permissionsData, rolesData] = await Promise.all([ + fetchJson(`/api/guilds/${state.selectedGuildId}/permissions`), + fetchJson(`/api/guilds/${state.selectedGuildId}/roles`), + ]); + + elements.permissionsEditor.value = JSON.stringify(permissionsData.permissions || {}, null, 2); + renderRoles(rolesData.roles || []); +}; + +const updatePermissions = async () => { + if (!state.selectedGuildId) { + return; + } + + const rawValue = elements.permissionsEditor.value.trim(); + if (!rawValue) { + return; + } + + let payload; + try { + payload = JSON.parse(rawValue); + } catch (error) { + elements.permissionsEditor.focus(); + throw new Error('Permissions JSON is invalid.'); + } + + const data = await fetchJson(`/api/guilds/${state.selectedGuildId}/permissions`, { + method: 'PATCH', + body: JSON.stringify(payload), + }); + + elements.permissionsEditor.value = JSON.stringify(data.permissions || {}, null, 2); +}; + +const loadWarnings = async (userId) => { + if (!state.selectedGuildId || !userId) { + return; + } + + const data = await fetchJson(`/api/guilds/${state.selectedGuildId}/warnings/${userId}`); + elements.warningsList.innerHTML = ''; + + const warnings = data.warnings || []; + if (warnings.length === 0) { + const item = document.createElement('li'); + item.textContent = 'No warnings found.'; + elements.warningsList.appendChild(item); + return; + } + + warnings.forEach((warning) => { + const item = document.createElement('li'); + const issuedAt = warning.createdAt ? new Date(warning.createdAt).toLocaleString() : 'Unknown date'; + item.textContent = `${warning.reason} — ${issuedAt}`; + elements.warningsList.appendChild(item); + }); +}; + +const addWarning = async () => { + if (!state.selectedGuildId) { + return; + } + + const userId = elements.newWarningUserId.value.trim(); + const reason = elements.warningReason.value.trim(); + + if (!userId || !reason) { + elements.warningResult.textContent = 'User ID and reason are required.'; + return; + } + + const data = await fetchJson(`/api/guilds/${state.selectedGuildId}/warnings/${userId}`, { + method: 'POST', + body: JSON.stringify({ reason }), + }); + + elements.warningResult.textContent = `Warning added: ${data.warning?.reason || reason}`; + elements.warningReason.value = ''; +}; + +const showError = (message) => { + elements.warningResult.textContent = message; +}; + +const init = async () => { + elements.loginButton.addEventListener('click', () => { + window.location.href = '/auth/discord'; + }); + + elements.logoutButton.addEventListener('click', async () => { + await fetch('/auth/logout', { method: 'POST', credentials: 'include' }); + setStatus('Logged out. Sign in to manage guilds.', false); + }); + + elements.guildSelect.addEventListener('change', async (event) => { + state.selectedGuildId = event.target.value; + await loadGuildDetails(); + }); + + elements.updatePermissionsButton.addEventListener('click', async () => { + elements.warningResult.textContent = ''; + try { + await updatePermissions(); + elements.warningResult.textContent = 'Permissions updated successfully.'; + } catch (error) { + showError(error.message); + } + }); + + elements.loadWarningsButton.addEventListener('click', async () => { + try { + await loadWarnings(elements.warningUserId.value.trim()); + } catch (error) { + showError(error.message); + } + }); + + elements.addWarningButton.addEventListener('click', async () => { + try { + await addWarning(); + } catch (error) { + showError(error.message); + } + }); + + try { + await loadGuilds(); + setStatus('Authenticated. Choose a guild to get started.', true); + } catch (error) { + setStatus('Sign in with Discord to manage your guilds.', false); + } +}; + +init(); diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..614f3d2 --- /dev/null +++ b/public/index.html @@ -0,0 +1,100 @@ + + + + + + Discord Bot Dashboard + + + +
+
+
+

Discord Bot

+

Moderation Dashboard

+

+ Manage permissions, review roles, and track warnings without leaving your browser. +

+
+ +
+ +
+
+

Session

+

Checking your session...

+
+ +
+ + + + + +
+

Need help?

+

+ The dashboard reads permissions from the MongoDB-backed overrides collection. Use it to grant + command access to trusted members in each server. +

+
+
+ + + + diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..e3c3758 --- /dev/null +++ b/public/styles.css @@ -0,0 +1,176 @@ +:root { + color-scheme: dark; + font-family: "Inter", system-ui, -apple-system, sans-serif; + line-height: 1.5; + background: #0e111b; + color: #eef2ff; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + background: radial-gradient(circle at top, #1d2542 0%, #0e111b 55%); +} + +.app { + max-width: 1100px; + margin: 0 auto; + padding: 48px 24px 64px; + display: flex; + flex-direction: column; + gap: 24px; +} + +.app__header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 24px; + padding: 32px; + border-radius: 20px; + background: rgba(17, 23, 40, 0.9); + border: 1px solid rgba(118, 135, 255, 0.2); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); +} + +.eyebrow { + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.2rem; + color: #8b9cff; + margin: 0 0 8px; +} + +h1 { + font-size: 2.4rem; + margin: 0 0 12px; +} + +.subhead { + margin: 0; + max-width: 520px; + color: #b7c0ff; +} + +.card { + background: rgba(17, 23, 40, 0.95); + border-radius: 18px; + padding: 24px; + border: 1px solid rgba(118, 135, 255, 0.2); + display: flex; + flex-direction: column; + gap: 16px; +} + +.card__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.card__content { + display: flex; + flex-direction: column; + gap: 16px; +} + +.columns { + display: grid; + gap: 16px; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); +} + +.panel { + background: rgba(12, 15, 26, 0.9); + border-radius: 14px; + padding: 16px; + border: 1px solid rgba(118, 135, 255, 0.15); + display: flex; + flex-direction: column; + gap: 12px; +} + +.button { + padding: 10px 18px; + border-radius: 999px; + border: 1px solid rgba(118, 135, 255, 0.3); + background: rgba(16, 20, 35, 0.8); + color: #f5f7ff; + cursor: pointer; + font-weight: 600; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.button:hover { + transform: translateY(-1px); + box-shadow: 0 8px 20px rgba(34, 44, 90, 0.4); +} + +.button--primary { + background: linear-gradient(135deg, #5f6bff, #8a5bff); + border: none; +} + +.button--ghost { + background: transparent; +} + +select, +input, +textarea { + width: 100%; + padding: 10px 12px; + border-radius: 10px; + border: 1px solid rgba(118, 135, 255, 0.3); + background: rgba(6, 8, 16, 0.6); + color: #eef2ff; +} + +textarea { + min-height: 140px; + resize: vertical; +} + +.list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +.list li { + background: rgba(16, 20, 35, 0.6); + padding: 8px 12px; + border-radius: 10px; + border: 1px solid rgba(118, 135, 255, 0.15); +} + +.field { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 0.85rem; +} + +.muted { + color: #9aa5e3; + margin: 0; +} + +@media (max-width: 720px) { + .app__header { + flex-direction: column; + align-items: flex-start; + } + + h1 { + font-size: 2rem; + } +} diff --git a/services/guildSettings.js b/services/guildSettings.js index 1d6cd3d..a6d6c53 100644 --- a/services/guildSettings.js +++ b/services/guildSettings.js @@ -15,7 +15,6 @@ const DEFAULT_GUILD_SETTINGS = { const normalizeGuildSettings = (settings = {}) => { const moderation = settings.moderation ?? {}; const strikeThresholds = moderation.strikeThresholds ?? {}; - return { ...DEFAULT_GUILD_SETTINGS, ...settings, @@ -34,12 +33,10 @@ const getGuildSettings = async (guildId) => { if (!guildId) { return normalizeGuildSettings(); } - const dbClient = await connectToDatabase(); const db = dbClient.db('discord'); const collection = db.collection('guild_settings'); const settings = await collection.findOne({ guildId }); - return normalizeGuildSettings(settings); };