diff --git a/.env.example b/.env.example
deleted file mode 100644
index bf94059..0000000
--- a/.env.example
+++ /dev/null
@@ -1,15 +0,0 @@
-# Backend configuration
-DISCORD_CLIENT_ID=
-DISCORD_CLIENT_SECRET=
-DISCORD_REDIRECT_URI=http://localhost:3000/auth/discord/callback
-DISCORD_BOT_TOKEN=
-MONGODB_URI=mongodb://localhost:27017
-SESSION_SECRET=change-me
-DASHBOARD_CLIENT_URL=http://localhost:5173
-DASHBOARD_PORT=3000
-DASHBOARD_SESSION_TTL_MS=86400000
-NODE_ENV=development
-
-# Frontend configuration (Vite)
-VITE_API_BASE_URL=http://localhost:3000
-VITE_BOT_INVITE_URL=
diff --git a/README.md b/README.md
index 0dcfe6d..fbaebc5 100644
--- a/README.md
+++ b/README.md
@@ -1,65 +1,80 @@
-# Discord Bot
+Im going to make a To-do List for the features i want to implement for my Bot.
-## Dashboard setup
+### **To-Do List for My Discord Bot**
-This repository includes a lightweight dashboard for configuring permissions and warnings without editing JSON. The dashboard uses Discord OAuth2 and the existing MongoDB collections.
+#### **Core Features**
+(I will try to make these tools optional, because alot of bots already feature them)
-### Environment variables
+1. **Basic Commands**
+ - [x] Add a `/help` command to display a list of available commands.
+ - [x] Implement a `/ping` command to check bot responsiveness.
+ - [x] Add an `/about` command to describe the bot's purpose.
-Copy `.env.example` and fill in the values:
+2. **Role Management**
+ - [ ] Set up a self-assignable role system (e.g., using reaction roles).
+ - [x] Allow admins to assign/remove roles with commands.
-```bash
-cp .env.example .env
-```
-
-Required backend variables:
-
-- `DISCORD_CLIENT_ID`
-- `DISCORD_CLIENT_SECRET`
-- `DISCORD_REDIRECT_URI` (example: `http://localhost:3000/auth/discord/callback`)
-- `DISCORD_BOT_TOKEN`
-- `MONGODB_URI`
-- `SESSION_SECRET`
-- `DASHBOARD_CLIENT_URL` (example: `http://localhost:5173`)
-
-Frontend variables (Vite):
-
-- `VITE_API_BASE_URL` (example: `http://localhost:3000`)
-- `VITE_BOT_INVITE_URL`
-
-> Make sure the redirect URI is added in the Discord Developer Portal OAuth2 settings.
-
-### Running the backend
-
-```bash
-node dashboardServer.js
-```
-
-The API will run on `DASHBOARD_PORT` (default `3000`).
-
-### Running the dashboard frontend
-
-```bash
-cd dashboard
-npm install
-npm run dev
-```
-
-The frontend will run on `http://localhost:5173` by default.
-
-### API health check
-
-```bash
-curl http://localhost:3000/health
-```
+3. **Moderation Tools**
+ - [x] Add commands to mute, kick, and temp_ban users.
+ - [ ] Create an auto-moderation feature to delete spam or inappropriate content.
+ - [x] Implement a warning system with a strike counter.
---
-## Existing feature notes
+#### **Unique Features**
+1. **Currency System**
+ - [x] Design a virtual currency system with basic commands:
+ - [x] `/balance` to check currency balance.
+ - [x] `/earn` to earn daily or periodic rewards.
+ - [x] `/transfer @user [amount]` to send currency to another user.
+ - [ ] Link the currency system to role management:
+ - [ ] Allow users to purchase roles using the currency with a command (e.g., `!buyrole [role name]`).
-### Server-Specific Permission Configuration
+2. **Datetime Converter**
+ (Still in Progress, maybe i'll add visual Menu)
+ - [x] Create a command to convert timezones:
+ - [x] `/timestamp [date]` to generate Discord timestamps.
+ - [x] Support multiple Discord timestamp formats.
+ - [ ] Include a command to display server-specific or user-specific timezones.
-Moderation commands support per-server permission overrides stored in MongoDB. Each guild can define which users or roles are allowed to run specific commands, plus optional admin overrides that apply to all moderation commands.
+3. **Minigames**
+ - [ ] Implement a Quiz Game:
+ - [x] Add a `/quiz` command to start the game.
+ - [x] Provide multiple-choice or true/false questions.
+ - [x] Award virtual currency for correct answers.
+ - [ ] Add a simple number-guessing game (e.g., `!guess [number]`).
+ - [ ] Create a leaderboard for game winners.
+
+---
+
+#### **Utility Tools**
+1. **Search and Lookups**
+ - [x] Add a `/wiki [topic]` command to fetch summaries from Wikipedia.
+ - [x] Include a `/weather [location]` command for weather updates.
+
+---
+
+#### **Advanced and Fun Features (very optional)**
+1. **AI Chat Integration**
+ - [ ] Integrate GPT for conversational AI:
+ - [ ] Respond to mentions with intelligent replies.
+ - [ ] Allow users to ask general questions with `!ask [question]`.
+
+---
+
+#### **Deployment (will happen if im completly satisfied with the bot)**
+1. **Hosting**
+ - [ ] Set up local hosting for development.
+ - [ ] Deploy the bot on a cloud platform (e.g., Heroku, AWS, or Railway).
+
+2. **Monitoring**
+ - [ ] Add logging to track errors and usage statistics.
+ - [ ] Implement uptime monitoring to ensure the bot stays online.
+
+---
+
+### **Server-Specific Permission Configuration**
+Moderation commands support per-server permission overrides stored in MongoDB. Each guild can define which users or roles are allowed to run specific commands, plus optional admin overrides that apply to all moderation commands. This is intended for future web-based configuration.
Example document stored in the `discord.guild_permissions` collection:
```json
diff --git a/dashboard/index.html b/dashboard/index.html
deleted file mode 100644
index 3dcf396..0000000
--- a/dashboard/index.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
- Discord Bot Dashboard
-
-
-
-
-
-
diff --git a/dashboard/package.json b/dashboard/package.json
deleted file mode 100644
index 19ed1d3..0000000
--- a/dashboard/package.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "name": "discord-bot-dashboard",
- "version": "0.1.0",
- "private": true,
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "preview": "vite preview"
- },
- "dependencies": {
- "react": "^18.3.1",
- "react-dom": "^18.3.1"
- },
- "devDependencies": {
- "@vitejs/plugin-react": "^4.3.1",
- "vite": "^5.4.7"
- }
-}
diff --git a/dashboard/src/App.jsx b/dashboard/src/App.jsx
deleted file mode 100644
index 6329058..0000000
--- a/dashboard/src/App.jsx
+++ /dev/null
@@ -1,576 +0,0 @@
-import { useCallback, useEffect, useMemo, useState } from 'react';
-
-const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
-const BOT_INVITE_URL = import.meta.env.VITE_BOT_INVITE_URL || '';
-
-const apiFetch = async (path, options = {}) => {
- const response = await fetch(`${API_BASE_URL}${path}`, {
- credentials: 'include',
- headers: {
- 'Content-Type': 'application/json',
- ...options.headers,
- },
- ...options,
- });
-
- const contentType = response.headers.get('content-type');
- const data = contentType && contentType.includes('application/json')
- ? await response.json()
- : null;
-
- if (!response.ok) {
- const error = new Error(data?.error || 'Request failed');
- error.details = data?.details;
- throw error;
- }
-
- return data;
-};
-
-const usePath = () => {
- const [path, setPath] = useState(window.location.pathname);
-
- useEffect(() => {
- const handler = () => setPath(window.location.pathname);
- window.addEventListener('popstate', handler);
- return () => window.removeEventListener('popstate', handler);
- }, []);
-
- const navigate = useCallback((nextPath) => {
- window.history.pushState({}, '', nextPath);
- setPath(nextPath);
- }, []);
-
- return { path, navigate };
-};
-
-const Toast = ({ message, type, onDismiss }) => {
- if (!message) return null;
- return (
-
- {message}
-
- ✕
-
-
- );
-};
-
-const Landing = () => (
-
-
Discord Bot Dashboard
-
Manage bot permissions and warnings without editing raw JSON.
-
-
-);
-
-const GuildPicker = ({ onManage }) => {
- const [guilds, setGuilds] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState('');
-
- useEffect(() => {
- let ignore = false;
- setLoading(true);
- apiFetch('/api/guilds')
- .then((data) => {
- if (!ignore) {
- setGuilds(data.guilds || []);
- setError('');
- }
- })
- .catch((err) => {
- if (!ignore) {
- setError(err.message);
- }
- })
- .finally(() => {
- if (!ignore) setLoading(false);
- });
-
- return () => {
- ignore = true;
- };
- }, []);
-
- return (
-
-
Your Guilds
-
Choose a server you manage to update permissions and warnings.
- {loading ?
Loading guilds…
: null}
- {error ?
{error}
: null}
-
- {guilds.map((guild) => (
-
- {guild.icon ? (
-
- ) : (
-
- )}
-
-
{guild.name}
- onManage(guild.id)}>
- Manage
-
-
-
- ))}
- {!loading && guilds.length === 0 ? (
-
No manageable guilds found for this account.
- ) : null}
-
-
- );
-};
-
-const AdminUserList = ({ value, onChange }) => {
- const [input, setInput] = useState('');
-
- const addUser = () => {
- const trimmed = input.trim();
- if (!trimmed) return;
- if (!value.includes(trimmed)) {
- onChange([...value, trimmed]);
- }
- setInput('');
- };
-
- return (
-
-
- {value.map((userId) => (
-
- {userId}
- onChange(value.filter((id) => id !== userId))}>
- ✕
-
-
- ))}
- {value.length === 0 ? No admin users yet. : null}
-
-
- setInput(event.target.value)}
- placeholder="Add user ID"
- />
-
- Add
-
-
-
- );
-};
-
-const CommandPermissionEditor = ({ roles, value, onChange }) => {
- const [newCommand, setNewCommand] = useState('');
- const commandEntries = Object.entries(value || {});
-
- const updateCommand = (commandName, nextData) => {
- onChange({
- ...value,
- [commandName]: {
- allowedRoleIds: nextData.allowedRoleIds,
- allowedUserIds: nextData.allowedUserIds,
- },
- });
- };
-
- const removeCommand = (commandName) => {
- const updated = { ...value };
- delete updated[commandName];
- onChange(updated);
- };
-
- const addCommand = () => {
- const trimmed = newCommand.trim();
- if (!trimmed) return;
- if (!value[trimmed]) {
- onChange({
- ...value,
- [trimmed]: { allowedRoleIds: [], allowedUserIds: [] },
- });
- }
- setNewCommand('');
- };
-
- return (
-
-
- setNewCommand(event.target.value)}
- placeholder="Add command name"
- />
-
- Add command
-
-
- {commandEntries.length === 0 ? (
-
No command overrides yet. Empty means commands are allowed for everyone.
- ) : null}
- {commandEntries.map(([commandName, entry]) => (
-
-
-
{commandName}
- removeCommand(commandName)}>
- Remove
-
-
-
- If both lists are empty, this command remains available to everyone.
-
-
-
Allowed roles
-
- {roles.map((role) => (
-
- {
- const next = event.target.checked
- ? [...entry.allowedRoleIds, role.id]
- : entry.allowedRoleIds.filter((id) => id !== role.id);
- updateCommand(commandName, {
- ...entry,
- allowedRoleIds: next,
- });
- }}
- />
- {role.name}
-
- ))}
- {roles.length === 0 ? No roles available. : null}
-
-
-
-
Allowed user IDs
-
updateCommand(commandName, {
- ...entry,
- allowedUserIds: nextUsers,
- })}
- />
-
-
- ))}
-
- );
-};
-
-const PermissionsTab = ({ guildId, onToast }) => {
- const [roles, setRoles] = useState([]);
- const [permissions, setPermissions] = useState(null);
- const [loading, setLoading] = useState(true);
- const [saving, setSaving] = useState(false);
- const [error, setError] = useState('');
-
- useEffect(() => {
- let ignore = false;
- setLoading(true);
- Promise.all([
- apiFetch(`/api/guilds/${guildId}/roles`),
- apiFetch(`/api/guilds/${guildId}/permissions`),
- ])
- .then(([rolesData, permissionsData]) => {
- if (ignore) return;
- setRoles(rolesData.roles || []);
- setPermissions(permissionsData.permissions || {});
- setError('');
- })
- .catch((err) => {
- if (!ignore) setError(err.message);
- })
- .finally(() => {
- if (!ignore) setLoading(false);
- });
-
- return () => {
- ignore = true;
- };
- }, [guildId]);
-
- const toggleRole = (roleId) => {
- if (!permissions) return;
- const adminRoleIds = permissions.adminRoleIds || [];
- const next = adminRoleIds.includes(roleId)
- ? adminRoleIds.filter((id) => id !== roleId)
- : [...adminRoleIds, roleId];
- setPermissions({ ...permissions, adminRoleIds: next });
- };
-
- const savePermissions = async () => {
- if (!permissions) return;
- setSaving(true);
- try {
- const response = await apiFetch(`/api/guilds/${guildId}/permissions`, {
- method: 'PATCH',
- body: JSON.stringify(permissions),
- });
- setPermissions(response.permissions);
- onToast('Permissions saved.', 'success');
- } catch (err) {
- onToast(err.message, 'error');
- } finally {
- setSaving(false);
- }
- };
-
- if (loading) return Loading permissions…
;
- if (error) return {error}
;
- if (!permissions) return Failed to load permissions.
;
-
- return (
-
-
-
Admin roles
-
- {roles.map((role) => (
-
- toggleRole(role.id)}
- />
- {role.name}
-
- ))}
- {roles.length === 0 ? No roles available. : null}
-
-
-
-
-
Admin users
-
setPermissions({ ...permissions, adminUserIds: nextUsers })}
- />
-
-
-
- Per-command permissions
-
- setPermissions({ ...permissions, commandPermissions: nextCommands })
- }
- />
-
-
-
-
- {saving ? 'Saving…' : 'Save permissions'}
-
-
-
- );
-};
-
-const WarningsTab = ({ guildId, onToast }) => {
- const [userId, setUserId] = useState('');
- const [warnings, setWarnings] = useState([]);
- const [loading, setLoading] = useState(false);
- const [reason, setReason] = useState('');
- const [error, setError] = useState('');
-
- const fetchWarnings = async () => {
- if (!userId.trim()) return;
- setLoading(true);
- try {
- const data = await apiFetch(`/api/guilds/${guildId}/warnings/${userId.trim()}`);
- setWarnings(data.warnings || []);
- setError('');
- } catch (err) {
- setError(err.message);
- } finally {
- setLoading(false);
- }
- };
-
- const addWarning = async () => {
- if (!userId.trim()) return;
- try {
- const data = await apiFetch(`/api/guilds/${guildId}/warnings/${userId.trim()}`, {
- method: 'POST',
- body: JSON.stringify({ reason }),
- });
- setWarnings(data.warnings || []);
- setReason('');
- onToast('Warning added.', 'success');
- } catch (err) {
- onToast(err.message, 'error');
- }
- };
-
- return (
-
-
-
User ID
-
- setUserId(event.target.value)}
- placeholder="Enter user ID"
- />
-
- {loading ? 'Loading…' : 'Load warnings'}
-
-
-
- {error ?
{error}
: null}
-
- {warnings.length === 0 ? (
-
No warnings found for this user.
- ) : (
- warnings.map((warning, index) => (
-
-
{warning.reason}
-
- Moderator: {warning.moderatorId || 'Unknown'}
- {warning.createdAt ? new Date(warning.createdAt).toLocaleString() : ''}
-
-
- ))
- )}
-
-
-
- Add warning
-
-
- );
-};
-
-const GuildSettings = ({ guildId }) => {
- const [activeTab, setActiveTab] = useState('permissions');
- const [toast, setToast] = useState({ message: '', type: 'success' });
-
- const dismissToast = () => setToast({ message: '', type: 'success' });
-
- return (
-
-
- setActiveTab('permissions')}
- >
- Permissions
-
- setActiveTab('warnings')}
- >
- Warnings
-
-
- {activeTab === 'permissions' ? (
-
setToast({ message, type })}
- />
- ) : (
- setToast({ message, type })}
- />
- )}
-
-
- );
-};
-
-const App = () => {
- const { path, navigate } = usePath();
- const [user, setUser] = useState(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState('');
-
- useEffect(() => {
- apiFetch('/api/me')
- .then((data) => {
- setUser(data.user);
- setError('');
- })
- .catch(() => {
- setUser(null);
- })
- .finally(() => setLoading(false));
- }, []);
-
- const guildId = useMemo(() => {
- const match = path.match(/^\/guilds\/(.+)$/);
- return match ? match[1] : null;
- }, [path]);
-
- if (loading) {
- return ;
- }
-
- if (!user) {
- return (
-
-
-
- );
- }
-
- return (
-
-
-
-
Welcome, {user.username}
-
{user.id}
-
- apiFetch('/auth/logout', { method: 'POST' }).then(() => window.location.reload())}
- >
- Log out
-
-
- {error ?
{error}
: null}
- {guildId ? (
-
- navigate('/')}>← Back to guilds
-
-
- ) : (
-
navigate(`/guilds/${id}`)} />
- )}
-
- );
-};
-
-export default App;
diff --git a/dashboard/src/main.jsx b/dashboard/src/main.jsx
deleted file mode 100644
index 89d5349..0000000
--- a/dashboard/src/main.jsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react';
-import { createRoot } from 'react-dom/client';
-import App from './App.jsx';
-import './styles.css';
-
-const rootElement = document.getElementById('root');
-createRoot(rootElement).render(
-
-
-
-);
diff --git a/dashboard/src/styles.css b/dashboard/src/styles.css
deleted file mode 100644
index bbf83e3..0000000
--- a/dashboard/src/styles.css
+++ /dev/null
@@ -1,265 +0,0 @@
-:root {
- font-family: 'Inter', system-ui, sans-serif;
- color: #e6e6f0;
- background-color: #0f1117;
-}
-
-* {
- box-sizing: border-box;
-}
-
-body {
- margin: 0;
- background: #0f1117;
-}
-
-a {
- color: inherit;
-}
-
-.container {
- max-width: 1080px;
- margin: 0 auto;
- padding: 32px 20px 64px;
-}
-
-.header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 16px;
- padding-bottom: 16px;
- border-bottom: 1px solid #24283b;
-}
-
-.card {
- background: #171b26;
- padding: 32px;
- border-radius: 16px;
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.35);
-}
-
-.button {
- background: #2f3348;
- border: none;
- color: #fff;
- padding: 10px 18px;
- border-radius: 10px;
- cursor: pointer;
- text-decoration: none;
- display: inline-flex;
- align-items: center;
- gap: 8px;
-}
-
-.button.primary {
- background: #5865f2;
-}
-
-.button.ghost {
- background: transparent;
- border: 1px solid #2f3348;
-}
-
-.text-button {
- background: none;
- border: none;
- color: #8b93ff;
- cursor: pointer;
- padding: 0;
-}
-
-.button-row {
- display: flex;
- flex-wrap: wrap;
- gap: 12px;
- margin-top: 16px;
-}
-
-.guild-grid {
- display: grid;
- gap: 16px;
- grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
- margin-top: 20px;
-}
-
-.guild-card {
- background: #171b26;
- padding: 16px;
- border-radius: 12px;
- display: flex;
- gap: 12px;
- align-items: center;
-}
-
-.guild-card img,
-.guild-placeholder {
- width: 56px;
- height: 56px;
- border-radius: 16px;
- object-fit: cover;
- background: #2f3348;
-}
-
-.guild-meta h3 {
- margin: 0 0 8px;
-}
-
-.muted {
- color: #9aa0b4;
- font-size: 0.95rem;
-}
-
-.error {
- color: #ff8a8a;
-}
-
-.form-group {
- margin-bottom: 24px;
-}
-
-.input-row {
- display: flex;
- gap: 12px;
- margin-top: 8px;
-}
-
-input,
-textarea {
- width: 100%;
- border-radius: 10px;
- border: 1px solid #2f3348;
- background: #0f1117;
- color: #e6e6f0;
- padding: 10px 12px;
-}
-
-textarea {
- resize: vertical;
-}
-
-.checkbox-grid {
- display: grid;
- gap: 8px;
- grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
- margin-top: 8px;
-}
-
-.checkbox-item {
- display: flex;
- gap: 8px;
- align-items: center;
- background: #0f1117;
- padding: 8px 10px;
- border-radius: 10px;
- border: 1px solid #2f3348;
-}
-
-.chip-row {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- margin-bottom: 10px;
-}
-
-.chip {
- background: #2f3348;
- padding: 6px 10px;
- border-radius: 999px;
- display: inline-flex;
- align-items: center;
- gap: 6px;
-}
-
-.chip button {
- border: none;
- background: none;
- color: inherit;
- cursor: pointer;
-}
-
-.command-card {
- background: #171b26;
- border-radius: 12px;
- padding: 16px;
- margin-top: 16px;
-}
-
-.command-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.tabs {
- display: flex;
- gap: 12px;
- margin: 20px 0;
-}
-
-.tabs button {
- background: #2f3348;
- border: none;
- color: #fff;
- padding: 10px 16px;
- border-radius: 10px;
- cursor: pointer;
-}
-
-.tabs button.active {
- background: #5865f2;
-}
-
-.warning-card {
- background: #171b26;
- padding: 12px 16px;
- border-radius: 10px;
- margin-bottom: 12px;
-}
-
-.warning-meta {
- display: flex;
- justify-content: space-between;
- font-size: 0.85rem;
- color: #9aa0b4;
- margin-top: 6px;
-}
-
-.toast {
- position: fixed;
- right: 24px;
- bottom: 24px;
- background: #2f3348;
- padding: 14px 18px;
- border-radius: 12px;
- display: flex;
- gap: 12px;
- align-items: center;
- box-shadow: 0 16px 40px rgba(0, 0, 0, 0.4);
-}
-
-.toast-success {
- border-left: 4px solid #4bd37b;
-}
-
-.toast-error {
- border-left: 4px solid #ff8a8a;
-}
-
-.toast button {
- background: none;
- border: none;
- color: inherit;
- cursor: pointer;
-}
-
-@media (max-width: 720px) {
- .input-row {
- flex-direction: column;
- }
-
- .header {
- flex-direction: column;
- align-items: flex-start;
- }
-}
diff --git a/dashboard/vite.config.js b/dashboard/vite.config.js
deleted file mode 100644
index 5c59447..0000000
--- a/dashboard/vite.config.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { defineConfig } from 'vite';
-import react from '@vitejs/plugin-react';
-
-export default defineConfig({
- plugins: [react()],
- server: {
- port: 5173,
- },
-});
diff --git a/dashboardServer.js b/dashboardServer.js
index b26c870..c094d01 100644
--- a/dashboardServer.js
+++ b/dashboardServer.js
@@ -2,18 +2,9 @@ const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const axios = require('axios');
-const cookieParser = require('cookie-parser');
-const cors = require('cors');
const express = require('express');
-const session = require('express-session');
-const MongoStore = require('connect-mongo');
-const rateLimit = require('express-rate-limit');
const {addWarning, getWarnings} = require('./services/warnings');
-const {
- getGuildPermissions,
- updateGuildPermissions,
- sanitizePermissionPayload,
-} = require('./services/permissions');
+const {getGuildPermissions, updateGuildPermissions} = require('./services/permissions');
const DEFAULT_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
const PERMISSION_BITS = {
ADMINISTRATOR: 0x8n,
@@ -47,8 +38,6 @@ const DISCORD_BOT_TOKEN =
process.env.DISCORD_BOT_TOKEN ||
config.discordBotToken ||
config.token;
-const MONGODB_URI = process.env.MONGODB_URI || config.mongodb;
-const SESSION_SECRET = process.env.SESSION_SECRET || config.sessionSecret;
const DASHBOARD_CLIENT_URL =
process.env.DASHBOARD_CLIENT_URL ||
config.dashboardClientUrl ||
@@ -67,14 +56,77 @@ console.log('[oauth config]', {
});
const DISCORD_API_BASE = 'https://discord.com/api';
-const DISCORD_OAUTH_BASE = 'https://discord.com/oauth2';
-const logInfo = (message, meta = {}) => {
- console.log(JSON.stringify({level: 'info', message, ...meta}));
+const sessionStore = new Map();
+
+const parseCookies = (headerValue) => {
+ const cookies = {};
+ if (!headerValue) {
+ return cookies;
+ }
+
+ headerValue.split(';').forEach((cookie) => {
+ const [rawName, ...rest] = cookie.trim().split('=');
+ if (!rawName) {
+ return;
+ }
+ cookies[rawName] = decodeURIComponent(rest.join('='));
+ });
+
+ return cookies;
};
-const logError = (message, meta = {}) => {
- console.error(JSON.stringify({level: 'error', message, ...meta}));
+const setCookie = (res, name, value, options = {}) => {
+ const directives = [`${name}=${encodeURIComponent(value)}`];
+ if (options.maxAge) {
+ directives.push(`Max-Age=${options.maxAge}`);
+ }
+ directives.push(`Path=${options.path || '/'}`);
+ if (options.httpOnly !== false) {
+ directives.push('HttpOnly');
+ }
+ if (options.sameSite) {
+ directives.push(`SameSite=${options.sameSite}`);
+ }
+ if (options.secure) {
+ directives.push('Secure');
+ }
+
+ res.append('Set-Cookie', directives.join('; '));
+};
+
+const clearCookie = (res, name) => {
+ setCookie(res, name, '', {maxAge: 0});
+};
+
+const createSession = (payload) => {
+ const sessionId = crypto.randomBytes(32).toString('hex');
+ const expiresAt = Date.now() + (Number.isNaN(SESSION_TTL_MS) ? DEFAULT_SESSION_TTL_MS : SESSION_TTL_MS);
+
+ sessionStore.set(sessionId, {
+ ...payload,
+ expiresAt,
+ });
+
+ return {sessionId, expiresAt};
+};
+
+const getSession = (sessionId) => {
+ const session = sessionStore.get(sessionId);
+ if (!session) {
+ return null;
+ }
+
+ if (session.expiresAt <= Date.now()) {
+ sessionStore.delete(sessionId);
+ return null;
+ }
+
+ return session;
+};
+
+const destroySession = (sessionId) => {
+ sessionStore.delete(sessionId);
};
const ensureOAuthConfig = () => {
@@ -99,7 +151,7 @@ const buildAuthorizationUrl = (state) => {
prompt: 'consent',
});
- return `${DISCORD_OAUTH_BASE}/authorize?${params.toString()}`;
+ return `${DISCORD_API_BASE}/oauth2/authorize?${params.toString()}`;
};
const fetchDiscordAccessToken = async (code) => {
@@ -175,10 +227,19 @@ const canManageGuild = (guild) => {
};
const ensureSession = (req, res, next) => {
- if (!req.session?.user || !req.session?.accessToken) {
+ const cookies = parseCookies(req.headers.cookie);
+ const sessionId = cookies.dashboard_session;
+ if (!sessionId) {
return res.status(401).json({error: 'Unauthorized'});
}
+ const session = getSession(sessionId);
+ if (!session) {
+ return res.status(401).json({error: 'Unauthorized'});
+ }
+
+ req.session = session;
+ req.sessionId = sessionId;
return next();
};
@@ -196,138 +257,40 @@ const ensureManageableGuild = async (req, res, next) => {
req.guild = guild;
return next();
} catch (error) {
- logError('Failed to fetch guilds for permission check', {
- message: error.message,
- status: error.response?.status,
- });
return res.status(502).json({error: 'Failed to fetch guilds'});
}
};
-const buildCorsOrigins = () => {
- if (!DASHBOARD_CLIENT_URL) {
- return true;
- }
- const entries = DASHBOARD_CLIENT_URL.split(',').map((value) => value.trim()).filter(Boolean);
- return entries.length ? entries : true;
-};
-
-const buildSessionStore = () => {
- if (!MONGODB_URI) {
- return null;
- }
- return MongoStore.create({
- mongoUrl: MONGODB_URI,
- collectionName: 'dashboard_sessions',
- ttl: Math.floor(
- (Number.isNaN(SESSION_TTL_MS) ? DEFAULT_SESSION_TTL_MS : SESSION_TTL_MS) / 1000,
- ),
- });
-};
-
-const validatePermissionPayload = (payload = {}) => {
- const errors = [];
- if (payload.adminRoleIds && !Array.isArray(payload.adminRoleIds)) {
- errors.push('adminRoleIds must be an array of role IDs');
- }
- if (payload.adminUserIds && !Array.isArray(payload.adminUserIds)) {
- errors.push('adminUserIds must be an array of user IDs');
- }
- if (payload.commandPermissions && typeof payload.commandPermissions !== 'object') {
- errors.push('commandPermissions must be an object');
- }
-
- return errors;
-};
-
-const validateWarningReason = (reason) => {
- if (typeof reason !== 'string') {
- return 'Reason must be a string';
- }
- const trimmed = reason.trim();
- if (!trimmed) {
- return 'Reason is required';
- }
- if (trimmed.length > 500) {
- return 'Reason must be 500 characters or fewer';
- }
- return null;
-};
-
const createDashboardApp = () => {
const app = express();
- const isProduction = process.env.NODE_ENV === 'production';
- app.set('trust proxy', 1);
-
- app.use(
- cors({
- origin: buildCorsOrigins(),
- credentials: true,
- }),
- );
app.use(express.json());
- app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
- if (!SESSION_SECRET) {
- logError('Missing SESSION_SECRET configuration');
- }
-
- const sessionStore = buildSessionStore();
- if (!sessionStore) {
- logError('Missing MONGODB_URI configuration for session store');
- }
-
- app.use(
- session({
- secret: SESSION_SECRET || crypto.randomBytes(32).toString('hex'),
- resave: false,
- saveUninitialized: false,
- cookie: {
- httpOnly: true,
- sameSite: 'lax',
- secure: isProduction,
- maxAge: Number.isNaN(SESSION_TTL_MS) ? DEFAULT_SESSION_TTL_MS : SESSION_TTL_MS,
- },
- store: sessionStore || undefined,
- }),
- );
-
- const authLimiter = rateLimit({
- windowMs: 60 * 1000,
- max: 30,
- standardHeaders: true,
- legacyHeaders: false,
- });
-
- app.get('/health', (req, res) => {
- return res.json({status: 'ok'});
- });
-
- app.get('/auth/discord', authLimiter, (req, res) => {
+ app.get('/auth/discord', (req, res) => {
if (!ensureOAuthConfig()) {
return res.status(500).json({error: 'Missing Discord OAuth configuration'});
}
const state = crypto.randomBytes(16).toString('hex');
- res.cookie('oauth_state', state, {
+ setCookie(res, 'oauth_state', state, {
httpOnly: true,
- sameSite: 'lax',
- secure: isProduction,
- maxAge: 300 * 1000,
+ sameSite: 'Lax',
+ secure: req.secure,
+ maxAge: 300,
});
return res.redirect(buildAuthorizationUrl(state));
});
- app.get('/auth/discord/callback', authLimiter, async (req, res) => {
+ app.get('/auth/discord/callback', async (req, res) => {
if (!ensureOAuthConfig()) {
return res.status(500).json({error: 'Missing Discord OAuth configuration'});
}
const {code, state} = req.query;
- if (!code || !state || req.cookies.oauth_state !== state) {
+ const cookies = parseCookies(req.headers.cookie);
+ if (!code || !state || cookies.oauth_state !== state) {
return res.status(400).json({error: 'Invalid OAuth state'});
}
@@ -335,15 +298,21 @@ const createDashboardApp = () => {
const tokenData = await fetchDiscordAccessToken(code);
const user = await fetchDiscordUser(tokenData.access_token);
- const expiresAt = Date.now() + (tokenData.expires_in || 0) * 1000;
- req.session.accessToken = tokenData.access_token;
- req.session.refreshToken = tokenData.refresh_token;
- req.session.tokenType = tokenData.token_type;
- req.session.scope = tokenData.scope;
- req.session.expiresAt = expiresAt;
- req.session.user = user;
+ const {sessionId, expiresAt} = createSession({
+ accessToken: tokenData.access_token,
+ refreshToken: tokenData.refresh_token,
+ tokenType: tokenData.token_type,
+ scope: tokenData.scope,
+ user,
+ });
- res.clearCookie('oauth_state');
+ setCookie(res, 'dashboard_session', sessionId, {
+ httpOnly: true,
+ sameSite: 'Lax',
+ secure: req.secure,
+ maxAge: Math.floor((expiresAt - Date.now()) / 1000),
+ });
+ clearCookie(res, 'oauth_state');
if (DASHBOARD_CLIENT_URL) {
return res.redirect(DASHBOARD_CLIENT_URL);
@@ -355,7 +324,7 @@ const createDashboardApp = () => {
return res.status(200).json({user});
} catch (error) {
- logError('Discord OAuth failed', {
+ console.error('Discord OAuth failed', {
message: error.message,
status: error.response?.status,
data: error.response?.data,
@@ -369,18 +338,9 @@ const createDashboardApp = () => {
});
app.post('/auth/logout', ensureSession, (req, res) => {
- req.session.destroy((err) => {
- if (err) {
- logError('Failed to destroy session', {message: err.message});
- return res.status(500).json({error: 'Failed to log out'});
- }
- res.clearCookie('connect.sid');
- return res.status(204).send();
- });
- });
-
- app.get('/api/me', ensureSession, (req, res) => {
- return res.json({user: req.session.user});
+ destroySession(req.sessionId);
+ clearCookie(res, 'dashboard_session');
+ return res.status(204).send();
});
app.get('/api/guilds', ensureSession, async (req, res) => {
@@ -389,10 +349,6 @@ const createDashboardApp = () => {
const manageableGuilds = guilds.filter(canManageGuild);
return res.json({guilds: manageableGuilds});
} catch (error) {
- logError('Failed to fetch guilds', {
- message: error.message,
- status: error.response?.status,
- });
return res.status(502).json({error: 'Failed to fetch guilds'});
}
});
@@ -406,7 +362,6 @@ const createDashboardApp = () => {
const permissions = await getGuildPermissions(req.params.guildId);
return res.json({permissions});
} catch (error) {
- logError('Failed to fetch permissions', {message: error.message});
return res.status(500).json({error: 'Failed to fetch permissions'});
}
},
@@ -417,19 +372,13 @@ const createDashboardApp = () => {
ensureSession,
ensureManageableGuild,
async (req, res) => {
- const validationErrors = validatePermissionPayload(req.body || {});
- if (validationErrors.length) {
- return res.status(400).json({error: 'Invalid permissions payload', details: validationErrors});
- }
try {
- const sanitizedPermissions = sanitizePermissionPayload(req.body || {});
const permissions = await updateGuildPermissions({
guildId: req.params.guildId,
- permissions: sanitizedPermissions,
+ permissions: req.body || {},
});
return res.json({permissions});
} catch (error) {
- logError('Failed to update permissions', {message: error.message});
return res.status(500).json({error: 'Failed to update permissions'});
}
},
@@ -444,10 +393,6 @@ const createDashboardApp = () => {
const roles = await fetchDiscordGuildRoles(req.params.guildId);
return res.json({roles});
} catch (error) {
- logError('Failed to fetch roles', {
- message: error.message,
- status: error.response?.status,
- });
return res.status(500).json({error: 'Failed to fetch roles'});
}
},
@@ -465,7 +410,6 @@ const createDashboardApp = () => {
});
return res.json({warnings});
} catch (error) {
- logError('Failed to fetch warnings', {message: error.message});
return res.status(500).json({error: 'Failed to fetch warnings'});
}
},
@@ -477,20 +421,18 @@ const createDashboardApp = () => {
ensureManageableGuild,
async (req, res) => {
const {reason} = req.body || {};
- const reasonError = validateWarningReason(reason);
- if (reasonError) {
- return res.status(400).json({error: reasonError});
+ if (!reason) {
+ return res.status(400).json({error: 'Reason is required'});
}
try {
const result = await addWarning({
guildId: req.params.guildId,
userId: req.params.userId,
moderatorId: req.session.user?.id,
- reason: reason.trim(),
+ reason,
});
return res.status(201).json(result);
} catch (error) {
- logError('Failed to add warning', {message: error.message});
return res.status(500).json({error: 'Failed to add warning'});
}
},
diff --git a/db.js b/db.js
index 0e69d54..1ad12b4 100644
--- a/db.js
+++ b/db.js
@@ -1,27 +1,11 @@
-const fs = require('node:fs');
-const path = require('node:path');
const {MongoClient, ServerApiVersion} = require('mongodb');
-
-const loadConfig = () => {
- const configPath = path.join(__dirname, 'config.json');
- if (fs.existsSync(configPath)) {
- return require(configPath);
- }
- return {};
-};
-
-const config = loadConfig();
-const MONGODB_URI = process.env.MONGODB_URI || config.mongodb;
+const {mongodb} = require("./config.json");
let dbClient;
async function connectToDatabase() {
if (!dbClient) {
- if (!MONGODB_URI) {
- throw new Error('Missing MongoDB connection string.');
- }
-
- dbClient = new MongoClient(MONGODB_URI, {
+ dbClient = new MongoClient(mongodb, {
serverApi: {
version: ServerApiVersion.v1,
strict: true,
diff --git a/package.json b/package.json
index 02c6974..afd5dba 100644
--- a/package.json
+++ b/package.json
@@ -3,8 +3,7 @@
"version": "1.0.0",
"main": "index.js",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1",
- "dashboard": "node dashboardServer.js"
+ "test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
@@ -13,13 +12,8 @@
"dependencies": {
"axios": "^1.7.7",
"better-sqlite3": "^11.3.0",
- "connect-mongo": "^5.1.0",
- "cookie-parser": "^1.4.6",
- "cors": "^2.8.5",
"discord.js": "^14.17.3",
"express": "^4.21.0",
- "express-rate-limit": "^7.4.0",
- "express-session": "^1.18.1",
"moment": "^2.30.1",
"mongodb": "^6.12.0",
"openai": "^4.67.3",
diff --git a/services/permissions.js b/services/permissions.js
index 1c8fa51..d794fc5 100644
--- a/services/permissions.js
+++ b/services/permissions.js
@@ -14,43 +14,6 @@ const normalizePermissionConfig = (config = {}) => ({
commandPermissions: config.commandPermissions ?? {},
});
-const sanitizePermissionPayload = (config = {}) => {
- const adminRoleIds = Array.isArray(config.adminRoleIds)
- ? config.adminRoleIds.filter((entry) => typeof entry === 'string' && entry.trim())
- : [];
- const adminUserIds = Array.isArray(config.adminUserIds)
- ? config.adminUserIds.filter((entry) => typeof entry === 'string' && entry.trim())
- : [];
- const commandPermissions = {};
-
- if (config.commandPermissions && typeof config.commandPermissions === 'object') {
- Object.entries(config.commandPermissions).forEach(([commandName, entry]) => {
- if (!commandName || typeof commandName !== 'string') {
- return;
- }
- if (!entry || typeof entry !== 'object') {
- return;
- }
- const allowedRoleIds = Array.isArray(entry.allowedRoleIds)
- ? entry.allowedRoleIds.filter((roleId) => typeof roleId === 'string' && roleId.trim())
- : [];
- const allowedUserIds = Array.isArray(entry.allowedUserIds)
- ? entry.allowedUserIds.filter((userId) => typeof userId === 'string' && userId.trim())
- : [];
- commandPermissions[commandName] = {
- allowedRoleIds,
- allowedUserIds,
- };
- });
- }
-
- return {
- adminRoleIds,
- adminUserIds,
- commandPermissions,
- };
-};
-
const memberHasRole = (member, roleIds) =>
roleIds.some((roleId) => member.roles?.cache?.has(roleId));
@@ -72,8 +35,7 @@ const updateGuildPermissions = async ({ guildId, permissions }) => {
const db = dbClient.db('discord');
const collection = db.collection('guild_permissions');
- const sanitizedPermissions = sanitizePermissionPayload(permissions);
- const normalizedPermissions = normalizePermissionConfig(sanitizedPermissions);
+ const normalizedPermissions = normalizePermissionConfig(permissions);
const payload = {
guildId,
...normalizedPermissions,
@@ -130,6 +92,4 @@ module.exports = {
getGuildPermissions,
updateGuildPermissions,
isCommandAllowed,
- normalizePermissionConfig,
- sanitizePermissionPayload,
};