Security & Contributing Guide
Overview
This guide covers the authentication architecture, security best practices, rate limiting implementation, and contribution workflow for QuranBot.
- Role-based permission system
- Rate limiting & cooldown implementation
- Security practices & token handling
- Contribution workflow & PR guidelines
Authentication Architecture
QuranBot implements a multi-layered authentication system via src/auth/auth-manager.js that
handles permissions at three levels: Discord native permissions, role-based keywords, and special developer
users.
Authentication Layers
| Layer | Implementation | Use Case |
|---|---|---|
hasAdminPermission() |
Checks PermissionsBitField.Flags.Administrator |
Native Discord admin permission (bypasses all checks) |
hasAdminRole() |
Keyword match against permissions_config.admin_roles |
Server-specific admin roles (e.g., "Quran Admin", "Islamic Admin") |
isSpecialUser() |
Checks global.SPE_USER_IDS from env |
Bot developers with global access across all servers |
Control Mode System
The bot supports two control modes per guild, stored in guildState.controlMode:
'admins' (Default)
- Only users with admin permission/role or special user ID can interact with control panel
- Public features (prayer times, azkar, guide) remain accessible to everyone
- Commands like
/إعداد,/دخول,/خروجrequire admin
'everyone'
- Any member can use navigation buttons (prev/next surah, reciter selection)
- Admin-only actions (join/leave voice, mode toggle) still require authorization
- Cooldowns apply to prevent abuse (90s for public actions in this mode)
// src/auth/auth-manager.js - Core authorization logic
function isAuthorized(interaction, guildState, interactionType) {
const member = interaction.member;
const userId = interaction.user?.id;
// Support/help always open
if (interactionType === 'support' || interactionType === 'open_complaint_modal') {
return true;
}
// Admin/special users bypass everything
if (hasAdminPermission(member) || hasAdminRole(member) || isSpecialUser(userId)) {
return true;
}
// Control mode check
if (!guildState?.controlMode || guildState.controlMode === 'admins') {
return false;
}
// Everyone mode - check whitelist
return allowed_everyone_actions.includes(interactionType);
}
Permission Flow Diagram
Every interaction passes through this authorization pipeline in
src/interactions/interactionProcessor.js:
-
Global Cooldown Check -
checkGlobalCooldown()blocks spam at user level -
Public Feature Detection -
isPublicFeature()allows azkar/prayer buttons without auth -
Voice State Validation -
checkVoiceState()ensures bot is in voice for playback actions -
Authorization Check -
checkAuthorization()applies permission logic -
Voice-Specific Cooldown -
checkVoiceCooldown()for join/leave actions - Handler Execution - Route to button/menu/command handler
Exempt Actions (No Auth Required)
These interaction types bypass authorization checks entirely:
-
submit_complaint,open_complaint_modal- Support flows play_azkar_*- Azkar audio playback buttons-
prayer_times,select_country_prayer,select_city_prayer- Prayer time selection -
more_features,back_to_main- Navigation helpers toggle_control_mode- Mode switching (handled separately)
Cooldown System Deep Dive
QuranBot implements a dual-layer cooldown system: per-command cooldowns and global rate limiting.
Command Cooldowns (src/state/commandCooldown.js)
| Command | Duration | Scope | Key Mapping |
|---|---|---|---|
/تحكم |
25s | Per user | control |
/دخول |
15s | Per user | join |
/خروج |
15s | Per user | leave |
/إعداد |
60s | Per server | setup |
/مواقيت_الصلاة |
50s | Per user | prayerTimes |
prayer_times button |
15s | Per user | prayerTimesButton |
Implementation Pattern
// In command handler (e.g., src/commands/join.js)
const cdResult = coreLoader.checkCooldown(
interaction.user.id,
interaction.guildId,
'join'
);
if (!cdResult.allowed) {
await safeError(
interaction,
coreLoader.getCooldownResponse(cdResult.remaining, cdResult.type)
);
return;
}
// ... execute command logic ...
// Set cooldown AFTER successful execution
coreLoader.setCooldown(interaction.user.id, interaction.guildId, 'join');
Memory Management
Cooldown data is stored in Map objects with automatic cleanup:
userCDMap: Keys are"userId:commandKey"serverCDMap: Keys are"guildId:commandKey"- Cleanup interval runs every 60s to remove expired entries
Rate Limiting Implementation
Beyond per-command cooldowns, QuranBot implements global rate limiting to prevent abuse across all interactions.
Global Rate Limit (src/state/cooldown.js)
// Default: 100 actions per 60 seconds per user+guild combination
function checkRateLimit(userId, guildId, maxActions = 100, windowMs = 60000) {
const now = Date.now();
const key = `${userId}:${guildId}`;
if (!rateLimitTracker.has(key)) {
rateLimitTracker.set(key, { count: 1, windowStart: now });
return { valid: true, count: 1, limit: maxActions };
}
const tracker = rateLimitTracker.get(key);
// Reset window if expired
if (now - tracker.windowStart > windowMs) {
tracker.count = 1;
tracker.windowStart = now;
return { valid: true, count: 1, limit: maxActions };
}
tracker.count++;
if (tracker.count > maxActions) {
return {
valid: false,
count: tracker.count,
limit: maxActions,
message: `تم تجاوز حد الاستخدام المسموح. يرجى الانتظار ${Math.ceil((windowMs - (now - tracker.windowStart)) / 1000)} ثانية`,
};
}
return { valid: true, count: tracker.count, limit: maxActions };
}
Interaction Deduplication
To prevent duplicate processing of rapid clicks,
src/interactions/interactionHandler.js implements a 500ms deduplication window:
// Generate unique interaction ID
const interactionId = `${interaction.guildId}-${interaction.user.id}-${interaction.id}`;
// Check if this exact interaction was processed recently
if (global.interactionRateLimits && global.interactionRateLimits.has(interactionId)) {
const lastTime = global.interactionRateLimits.get(interactionId);
const now = Date.now();
if (now - lastTime < 500) { // 500ms minimum interval
coreLoader.logger.debug(`Ignored Fast Duplicate Interaction ${interactionId}`);
return;
}
}
// Record this interaction
global.interactionRateLimits.set(interactionId, Date.now());
Rate limit data is stored in-memory only and resets on bot restart. This is intentional: persistent rate
limiting across restarts could unfairly penalize users after maintenance. For production deployments
requiring persistent rate limiting, consider integrating with Redis using the
@database/redis module.
Security Best Practices
Environment Security
-
Never commit secrets - Use
.env.examplefiles as templates only -
Separate environments -
development.envandproduction.envprevent accidental use of production tokens in dev -
TOPGG_TOKEN stripping -
envSwitcher.jsautomatically removes this token in development mode - Rotate tokens regularly - Especially after team member changes or suspected compromise
Input Validation Patterns
All user inputs that affect bot behavior should be validated before use:
// Example: Validating surah selection in menu handler
const selectedSurahNum = parseInt(interaction.values[0]);
// Validate range
if (selectedSurahNum < 1 || selectedSurahNum > global.surahNames.length) {
await safeError(interaction, 'Invalid surah selection');
return;
}
// Validate availability for current reciter
if (!isSurahAvailable(guildState, surahIndex)) {
await safeError(interaction, 'This surah is not available for the current reciter');
return;
}
SQL/NoSQL Injection Prevention
Since QuranBot uses Firebase Realtime Database (NoSQL), injection risks are lower, but still follow these practices:
-
Use
deepCloneForFirebase()to sanitize objects before storage - Never use user input directly in database paths - always validate and sanitize
- Use Firebase security rules (
firebase.rules.json) to deny public access
Discord API Security
-
Use ephemeral replies for sensitive information (
flags: 64) - Validate interaction ownership - Ensure buttons/menus are used by the original requester when appropriate
-
Handle interaction expiration - Catch
10062 Unknown interactionerrors gracefully - Limit embed content - Sanitize user-generated content before including in embeds
Token & Secret Handling
Firebase Private Key Sanitization
The getSanitizedPrivateKey() function in src/database/firebase/client.js handles
common formatting issues:
function getSanitizedPrivateKey() {
const rawKey = process.env.FIREBASE_ADMIN_PRIVATE_KEY;
if (!rawKey) return '';
let key = rawKey;
// Fix escaped newlines from dotenv parsing
key = key.replace(/\\n/g, '\n');
key = key.replace(/\\r\\n/g, '\n');
// Remove surrounding quotes if present
if (key.startsWith('"') && key.endsWith('"')) {
key = key.slice(1, -1);
}
// Ensure trailing newline for PEM format
if (key && !key.endsWith('\n')) {
key += '\n';
}
return key;
}
Token Loading Flow
.envsetsNODE_ENV(development/production)-
envSwitcher.jsloads the corresponding*.envfile -
In development:
TOPGG_TOKENis stripped fromprocess.env - Bot modules access tokens via
process.envorglobal.token
For cloud deployments (Heroku, Railway, etc.), set environment variables via the platform's dashboard or
CLI, not via .env files. Never commit .env files to version control.
Input Validation Patterns
Common Validation Utilities
| Utility | Location | Use Case |
|---|---|---|
validateReciterData() |
src/data/data-loader-validator.js |
Ensures reciter has required rewaya_id and server |
validateRadioData() |
src/data/data-loader-validator.js |
Checks radio entry has non-empty url |
validateSurahId() |
src/data/data-loader-validator.js |
Validates surah ID is between 1-114 |
clean_Dhikr() |
src/helpers/azkar.js |
Removes annotation chars from dhikr text before display |
Sanitization for Display
When displaying user-generated or external content, sanitize to prevent XSS or layout issues:
// Example: Truncating text for Discord embed fields (max 1024 chars)
function truncateText(text, maxLength) {
if (!text) return '';
const str = String(text);
return str.length > maxLength ? str.substring(0, maxLength) : str;
}
// Usage in UI components
const safeLabel = truncateText(reciter.name, 100);
URL Validation for Audio Streams
Before playing audio, validate stream URLs to prevent playback errors:
// src/audio/resource.js - validateStreamUrl function
async function validateStreamUrl(url) {
if (!url?.startsWith('http')) return { valid: false, reason: 'Invalid URL' };
try {
const client = require('@startup/botSetup').client;
if (!client.lavalink) return { valid: false, reason: 'Lavalink unavailable' };
const bestNode = getBestNode(client.lavalink);
const nodes = bestNode ? [bestNode] : client.lavalink.nodeManager.leastUsedNodes('players').filter((n) => n.connected);
if (!nodes.length) return { valid: false, reason: 'No connected nodes' };
const result = await nodes[0].search({ query: url, source: 'http' }, client.user);
return { valid: result.tracks && result.tracks.length > 0, reason: 'OK' };
} catch (error) {
return { valid: false, reason: error.message };
}
}
Contributing Guidelines
Before You Start
- Check existing issues - Search GitHub for similar feature requests or bug reports
- Discuss major changes - Open an issue or join Discord to discuss architecture changes before coding
- Test locally - Ensure your changes work in a test server before submitting
Branch Naming Convention
| Branch Type | Format | Example |
|---|---|---|
| Feature | feature/descriptive-name |
feature/add-surah-favorites |
| Bug Fix | fix/issue-description |
fix/voice-disconnect-recovery |
| Documentation | docs/section-updated |
docs/add-security-guide |
| Refactor | refactor/module-name |
refactor/auth-manager-cleanup |
Commit Message Format
# Format: type(scope): brief description
# Types: feat, fix, docs, style, refactor, test, chore
# Good examples:
feat(audio): add fallback reciter selection on stream failure
fix(auth): handle expired interactions in permission checks
docs(security): add token handling best practices guide
refactor(state): simplify guild state initialization logic
# Avoid:
"fixed stuff"
"update code"
"wip"
Pull Request Workflow
Fork & Clone
Fork the repository and clone your fork locally. Add upstream remote for syncing.
Create Feature Branch
Use the branch naming convention. Keep branches focused on a single change.
Implement & Test
Write code following existing patterns. Test in a real Discord server. Run
pnpm run dev for hot reload.
Run Linting
Ensure code passes style checks: pnpm run lint (if configured) or follow
prettier.config.js.
Submit PR
Open PR with clear title, description of changes, and any relevant issue links. Request review from maintainers.
Address Feedback
Respond to review comments promptly. Push additional commits to the same branch; they'll auto-append to the PR.
Code Style & Standards
JavaScript Conventions
-
Indentation: 4 spaces (configured in
prettier.config.js) - Quotes: Single quotes for strings, template literals for interpolation
- Semicolons: Always use semicolons
- Trailing commas: Use in multiline objects/arrays
- Arrow functions: Use for callbacks and short functions; named functions for complex logic
Module Organization
// 1. External dependencies first
const { EmbedBuilder } = require('discord.js');
// 2. Internal modules with path aliases
require('pathlra-aliaser')();
const logger = require('@logging/logger');
const { safeReply } = require('@interactions/flow/responder');
// 3. Local relative imports last
const { createControlEmbed } = require('../ui/embeds');
// 4. Module exports at bottom
module.exports = { execute };
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Variables/Functions | camelCase |
createSurahResource, guildState |
| Constants | UPPER_SNAKE_CASE |
COMMAND_COOLDOWNS, MAX_RETRY_ATTEMPTS |
| Classes | PascalCase |
PersistentStateManager, VoiceLogger |
| Files | kebab-case.js |
guild-state-store.js, commandCooldown.js |
| Commands | Arabic names for user-facing, English keys internally | /تحكم → 'control' |
Testing Requirements
Manual Testing Checklist
Before submitting a PR, verify these scenarios in a test Discord server:
- ☐ Command works with valid inputs
- ☐ Command handles invalid inputs gracefully (error message, no crash)
- ☐ Permissions are enforced correctly (admin vs. everyone mode)
- ☐ Cooldowns trigger appropriately
- ☐ Voice operations work (join, play, pause, leave)
- ☐ State persists across bot restarts (if applicable)
- ☐ No memory leaks after repeated use (monitor with
/سرعة)
Logging for Debugging
Use the centralized logger instead of console.log for consistent output:
const logger = require('@logging/logger');
// Log levels: debug, info, warn, error, fatal
logger.info(`User ${userId} triggered command ${commandName}`);
logger.warn(`Fallback reciter used for guild ${guildId}`);
logger.error('Failed to fetch surah', error, { surahIndex, reciter });
// Voice-specific logging
const voiceLogger = require('@logging/voiceLogger');
voiceLogger.connection(guildId, 'Voice connection established', { channelId });
Using the /سرعة Diagnostic Command
This command provides real-time metrics for debugging:
- Bot latency, WebSocket ping, Discord API response time
- Uptime, server count, total users
- Voice connection count, CPU usage
- Lavalink node status (ping, players, uptime)
- Commands used, azkar sent (from Firebase stats)
Debugging Techniques
Enabling Debug Logging
Set LOG_LEVEL=debug in your environment to see detailed logs:
# In .env or command line LOG_LEVEL=debug NODE_ENV=development pnpm run dev # Logs will show: # - Interaction routing decisions # - Voice connection state changes # - Cache hits/misses # - Database read/write operations
Voice-Specific Debugging
Use voiceLogger for granular audio debugging:
const voiceLogger = require('@logging/voiceLogger');
// Log connection events
voiceLogger.connection(guildId, 'Initializing Lavalink player', {
channelId: targetChannel.id,
guildName: targetChannel.guild?.name,
});
// Log playback events
voiceLogger.player(guildId, 'Track started', { title: track.info?.title });
// Log resource creation
voiceLogger.resource(guildId, 'Creating surah resource', {
surahIndex,
reciter: state.currentReciter,
});
Common Debugging Scenarios
Bot not responding to commands
Check:
-
Bot has
Application (bot)scope withapplications.commandsin OAuth URL - Commands are registered (
logger.infoshould show count) - Bot has
Message Contentintent enabled in Discord Developer Portal - No interaction cooldown or rate limit blocking the request
Voice connection fails with "No compatible encryption modes"
This indicates a Lavalink version mismatch. Verify:
- Lavalink server is v4.x (required for Discord's current encryption)
securesetting matches your deployment (false for HTTP, true for HTTPS)- Bot and Lavalink are on compatible Node.js versions (v22 recommended)
Use /سرعة to check node status and error messages.
State not persisting after restart
Verify:
- Firebase credentials are correct in
production.env global.saveRuntimeStatesis called after state changes- Redis is available (if used) for hot caching
- Check
storage/logs/for Firebase connection errors
Test by making a state change, restarting the bot, and verifying the state is restored.
Memory Profiling
Use Node.js built-in tools to identify memory leaks:
# Start bot with heap snapshot capability
node --heap-prof src/bot/core.js
# Or use Chrome DevTools
node --inspect src/bot/core.js
# Then open chrome://inspect in Chrome
# Monitor memory usage via /سرعة command
# Alert if heapUsed exceeds 1.5GB (triggers aggressive GC)
Never enable LOG_LEVEL=debug in production environments. Use structured logging with
appropriate levels (info, warn, error) and rely on the
/سرعة command for operational monitoring.