๐ Authentication
+โ ๏ธ Note: This demo requires Discord OAuth2 authentication. In production, implement proper OAuth2 flow.
+diff --git a/.env.example b/.env.example
index 9fce381..985959d 100644
--- a/.env.example
+++ b/.env.example
@@ -24,6 +24,7 @@ ENABLE_GENERAL_MODULE=true
ENABLE_MODERATION_MODULE=true
ENABLE_LFG_MODULE=true
ENABLE_REMINDERS_MODULE=true
+ENABLE_WEB_MODULE=true
# Alternative: Use comma-separated list to override all enabled modules
# ENABLED_MODULES=ai,information,music,reminders
@@ -54,3 +55,8 @@ PREMIUM_COMMAND_COOLDOWN=1000
# Development
NODE_ENV=production
+
+# Web Dashboard Configuration
+WEB_PORT=3000
+WEB_SECRET=your_web_dashboard_secret_here
+DASHBOARD_URL=http://localhost:3001
diff --git a/.gitignore b/.gitignore
index fa07dfe..95b481a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -95,3 +95,6 @@ test/test-*.js
.aider*
AGENTS.md
+
+# Dashboard frontend (Next.js)
+dashboard/
diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md
new file mode 100644
index 0000000..7a64323
--- /dev/null
+++ b/API_DOCUMENTATION.md
@@ -0,0 +1,894 @@
+# DeepQuasar Dashboard API Documentation
+
+This API provides comprehensive endpoints for managing your Discord server through the DeepQuasar dashboard. All endpoints require authentication and guild access validation.
+
+## Base URL
+
+```
+http://localhost:3000/api
+```
+
+## Authentication
+
+All API requests (except health check) require a Bearer token in the Authorization header:
+
+```http
+Authorization: Bearer Interactive demonstration of the DeepQuasar Bot API endpoints โ ๏ธ Note: This demo requires Discord OAuth2 authentication. In production, implement proper OAuth2 flow.
+ Next step: Use the ๐ DeepQuasar Dashboard API Demo
+ ๐ Authentication
+ ๐ System Health
+ ๐ฐ Guild Management
+ ๐ต Music Control
+ OAuth2 Error
+ ${error}
+
+
+ `);
+ }
+
+ if (!code) {
+ return res.send(`
+
+ No code provided.
+
+
+ `);
+ }
+
+ if (!clientId || !clientSecret) {
+ return res.send(`
+
+ Config Error
+ DISCORD_CLIENT_ID and DISCORD_CLIENT_SECRET must be set in environment
+
+
+ `);
+ }
+
+ try {
+ const params = new URLSearchParams();
+ params.append('grant_type', 'authorization_code');
+ params.append('code', code);
+ params.append('redirect_uri', redirectUri);
+
+ const axios = require('axios');
+ const tokenRes = await axios.post('https://discord.com/api/oauth2/token', params.toString(), {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ auth: {
+ username: clientId,
+ password: clientSecret
+ }
+ });
+
+ const tokenData = tokenRes.data;
+
+ res.send(`
+
+ Authentication successful!
+ Access Token Response:
+ ${JSON.stringify(tokenData, null, 2)}
+ access_token above to POST to /api/auth/login with a JSON body:
+ {
+ "accessToken": "${tokenData.access_token}",
+ "guildId": "YOUR_GUILD_ID"
+}
+
+ The response will include a token (JWT) to use with /api/auth/verify and other endpoints.
+
+ Example curl:
+ curl -X POST http://localhost:3000/api/auth/login -H "Content-Type: application/json" -d '{"accessToken": "${tokenData.access_token}", "guildId": "YOUR_GUILD_ID"}'
+
You may close this window.
+ + + `); + } catch (err) { + res.send(` + +${err.response?.data ? JSON.stringify(err.response.data, null, 2) : err.message}
+
+
+ `);
+ }
+});
+
+/**
+ * GET /api/auth/me
+ * Returns the authenticated user's Discord info and permissions in the current guild.
+ * Requires a valid JWT in Authorization header.
+ */
+router.get('/me', verifyToken, validateGuildAccess, (req, res) => {
+ res.json({
+ success: true,
+ user: {
+ id: req.member.user.id,
+ username: req.member.user.username,
+ displayName: req.member.displayName,
+ avatar: req.member.user.displayAvatarURL(),
+ permissions: {
+ administrator: req.member.permissions.has('Administrator'),
+ manageGuild: req.member.permissions.has('ManageGuild'),
+ moderateMembers: req.member.permissions.has('ModerateMembers'),
+ manageMessages: req.member.permissions.has('ManageMessages')
+ }
+ },
+ guild: {
+ id: req.guild.id,
+ name: req.guild.name,
+ icon: req.guild.iconURL(),
+ memberCount: req.guild.memberCount
+ }
+ });
+});
+
+/**
+ * GET /api/auth/ping
+ * Simple health check endpoint.
+ */
+router.get('/ping', (req, res) => {
+ res.json({ success: true, message: 'pong', timestamp: Date.now() });
+});
+
+module.exports = router;
diff --git a/src/modules/web/routes/guild.js b/src/modules/web/routes/guild.js
new file mode 100644
index 0000000..a787cd2
--- /dev/null
+++ b/src/modules/web/routes/guild.js
@@ -0,0 +1,281 @@
+/**
+ * Guild Management Routes
+ */
+
+const express = require('express');
+const router = express.Router();
+const { verifyToken, validateGuildAccess, requireAdmin, rateLimit } = require('../middleware/auth');
+const Guild = require('../../../schemas/Guild');
+
+// Apply authentication and rate limiting to all routes
+router.use(verifyToken);
+router.use(rateLimit(60000, 50)); // 50 requests per minute
+
+/**
+ * GET /api/guild/:guildId
+ * Get guild information and bot settings
+ */
+router.get('/:guildId', validateGuildAccess, async (req, res) => {
+ try {
+ const guildSettings = await Guild.findByGuildId(req.guild.id) ||
+ await Guild.createDefault(req.guild.id, req.guild.name);
+
+ res.json({
+ success: true,
+ guild: {
+ id: req.guild.id,
+ name: req.guild.name,
+ icon: req.guild.iconURL(),
+ memberCount: req.guild.memberCount,
+ botJoinedAt: req.guild.joinedAt,
+ features: req.guild.features,
+ settings: guildSettings
+ }
+ });
+
+ } catch (error) {
+ req.client.logger.error('Guild fetch error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to fetch guild information'
+ });
+ }
+});
+
+/**
+ * PUT /api/guild/:guildId/settings
+ * Update guild settings (admin only)
+ */
+router.put('/:guildId/settings', validateGuildAccess, requireAdmin, async (req, res) => {
+ try {
+ const updates = req.body;
+
+ // Find or create guild settings
+ let guildSettings = await Guild.findByGuildId(req.guild.id);
+ if (!guildSettings) {
+ guildSettings = await Guild.createDefault(req.guild.id, req.guild.name);
+ }
+
+ // Update allowed fields
+ const allowedFields = [
+ 'musicSettings',
+ 'commandSettings',
+ 'queueSettings',
+ 'logging',
+ 'chatbot',
+ 'messageLinkEmbed',
+ 'welcomeSystem',
+ 'autoRole',
+ 'permissions'
+ ];
+
+ for (const field of allowedFields) {
+ if (updates[field] !== undefined) {
+ guildSettings[field] = { ...guildSettings[field], ...updates[field] };
+ }
+ }
+
+ await guildSettings.save();
+
+ res.json({
+ success: true,
+ message: 'Guild settings updated successfully',
+ settings: guildSettings
+ });
+
+ } catch (error) {
+ req.client.logger.error('Guild settings update error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to update guild settings'
+ });
+ }
+});
+
+/**
+ * GET /api/guild/:guildId/channels
+ * Get list of channels in the guild
+ */
+router.get('/:guildId/channels', validateGuildAccess, (req, res) => {
+ try {
+ const channels = req.guild.channels.cache
+ .filter(channel => channel.type !== 4) // Exclude category channels
+ .map(channel => ({
+ id: channel.id,
+ name: channel.name,
+ type: channel.type,
+ position: channel.position,
+ parentId: channel.parentId,
+ permission: {
+ viewChannel: channel.permissionsFor(req.member)?.has('ViewChannel') || false,
+ sendMessages: channel.permissionsFor(req.member)?.has('SendMessages') || false,
+ manageChannel: channel.permissionsFor(req.member)?.has('ManageChannels') || false
+ }
+ }))
+ .sort((a, b) => a.position - b.position);
+
+ res.json({
+ success: true,
+ channels
+ });
+
+ } catch (error) {
+ req.client.logger.error('Channels fetch error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to fetch guild channels'
+ });
+ }
+});
+
+/**
+ * GET /api/guild/:guildId/roles
+ * Get list of roles in the guild
+ */
+router.get('/:guildId/roles', validateGuildAccess, (req, res) => {
+ try {
+ const roles = req.guild.roles.cache
+ .filter(role => role.id !== req.guild.id) // Exclude @everyone role
+ .map(role => ({
+ id: role.id,
+ name: role.name,
+ color: role.hexColor,
+ position: role.position,
+ permissions: role.permissions.toArray(),
+ managed: role.managed,
+ mentionable: role.mentionable,
+ memberCount: role.members.size
+ }))
+ .sort((a, b) => b.position - a.position);
+
+ res.json({
+ success: true,
+ roles
+ });
+
+ } catch (error) {
+ req.client.logger.error('Roles fetch error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to fetch guild roles'
+ });
+ }
+});
+
+/**
+ * GET /api/guild/:guildId/members
+ * Get list of members in the guild (with pagination)
+ */
+router.get('/:guildId/members', validateGuildAccess, (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = Math.min(parseInt(req.query.limit) || 20, 100);
+ const search = req.query.search?.toLowerCase();
+
+ let members = Array.from(req.guild.members.cache.values());
+
+ // Apply search filter
+ if (search) {
+ members = members.filter(member =>
+ member.user.username.toLowerCase().includes(search) ||
+ member.displayName.toLowerCase().includes(search) ||
+ member.user.id.includes(search)
+ );
+ }
+
+ // Sort by join date (newest first)
+ members.sort((a, b) => b.joinedTimestamp - a.joinedTimestamp);
+
+ // Apply pagination
+ const startIndex = (page - 1) * limit;
+ const endIndex = startIndex + limit;
+ const paginatedMembers = members.slice(startIndex, endIndex);
+
+ const memberData = paginatedMembers.map(member => ({
+ id: member.user.id,
+ username: member.user.username,
+ displayName: member.displayName,
+ avatar: member.user.displayAvatarURL(),
+ joinedAt: member.joinedAt,
+ roles: member.roles.cache
+ .filter(role => role.id !== req.guild.id)
+ .map(role => ({
+ id: role.id,
+ name: role.name,
+ color: role.hexColor
+ })),
+ permissions: {
+ administrator: member.permissions.has('Administrator'),
+ manageGuild: member.permissions.has('ManageGuild'),
+ moderateMembers: member.permissions.has('ModerateMembers')
+ }
+ }));
+
+ res.json({
+ success: true,
+ members: memberData,
+ pagination: {
+ page,
+ limit,
+ total: members.length,
+ totalPages: Math.ceil(members.length / limit),
+ hasNext: endIndex < members.length,
+ hasPrev: page > 1
+ }
+ });
+
+ } catch (error) {
+ req.client.logger.error('Members fetch error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to fetch guild members'
+ });
+ }
+});
+
+/**
+ * GET /api/guild/:guildId/stats
+ * Get guild statistics
+ */
+router.get('/:guildId/stats', validateGuildAccess, async (req, res) => {
+ try {
+ const guildSettings = await Guild.findByGuildId(req.guild.id);
+
+ // Calculate basic stats
+ const stats = {
+ members: {
+ total: req.guild.memberCount,
+ online: req.guild.members.cache.filter(m => m.presence?.status === 'online').size,
+ bots: req.guild.members.cache.filter(m => m.user.bot).size
+ },
+ channels: {
+ total: req.guild.channels.cache.size,
+ text: req.guild.channels.cache.filter(c => c.type === 0).size,
+ voice: req.guild.channels.cache.filter(c => c.type === 2).size,
+ categories: req.guild.channels.cache.filter(c => c.type === 4).size
+ },
+ roles: req.guild.roles.cache.size - 1, // Exclude @everyone
+ bot: {
+ joinedAt: req.guild.joinedAt,
+ commandsUsed: guildSettings?.stats?.commandsUsed || 0,
+ songsPlayed: guildSettings?.stats?.songsPlayed || 0,
+ totalPlaytime: guildSettings?.stats?.totalPlaytime || 0,
+ lastActivity: guildSettings?.stats?.lastActivity
+ }
+ };
+
+ res.json({
+ success: true,
+ stats
+ });
+
+ } catch (error) {
+ req.client.logger.error('Stats fetch error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to fetch guild statistics'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/src/modules/web/routes/lfg.js b/src/modules/web/routes/lfg.js
new file mode 100644
index 0000000..40e2c37
--- /dev/null
+++ b/src/modules/web/routes/lfg.js
@@ -0,0 +1,166 @@
+/**
+ * LFG (Looking for Group) Routes
+ */
+
+const express = require('express');
+const router = express.Router();
+const { verifyToken, validateGuildAccess, requireModerator, rateLimit } = require('../middleware/auth');
+const LFGPost = require('../../../schemas/LFGPost');
+const LFGSettings = require('../../../schemas/LFGSettings');
+
+// Apply authentication and rate limiting to all routes
+router.use(verifyToken);
+router.use(rateLimit(60000, 50)); // 50 requests per minute
+
+/**
+ * GET /api/lfg/:guildId/posts
+ * Get LFG posts
+ */
+router.get('/:guildId/posts', validateGuildAccess, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = Math.min(parseInt(req.query.limit) || 20, 100);
+ const game = req.query.game;
+ const status = req.query.status || 'active'; // active, filled, expired
+
+ const query = { guildId: req.guild.id };
+ if (game) query.game = new RegExp(game, 'i');
+ if (status !== 'all') query.status = status;
+
+ const total = await LFGPost.countDocuments(query);
+ const posts = await LFGPost.find(query)
+ .sort({ createdAt: -1 })
+ .limit(limit)
+ .skip((page - 1) * limit);
+
+ res.json({
+ success: true,
+ posts,
+ pagination: {
+ page,
+ limit,
+ total,
+ totalPages: Math.ceil(total / limit)
+ }
+ });
+
+ } catch (error) {
+ req.client.logger.error('LFG posts fetch error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to fetch LFG posts'
+ });
+ }
+});
+
+/**
+ * GET /api/lfg/:guildId/settings
+ * Get LFG settings
+ */
+router.get('/:guildId/settings', validateGuildAccess, requireModerator, async (req, res) => {
+ try {
+ const settings = await LFGSettings.findOne({ guildId: req.guild.id });
+
+ res.json({
+ success: true,
+ settings: settings || {
+ enabled: false,
+ channelId: null,
+ autoDeleteAfter: 24,
+ allowedGames: [],
+ maxPostsPerUser: 3
+ }
+ });
+
+ } catch (error) {
+ req.client.logger.error('LFG settings fetch error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to fetch LFG settings'
+ });
+ }
+});
+
+/**
+ * PUT /api/lfg/:guildId/settings
+ * Update LFG settings
+ */
+router.put('/:guildId/settings', validateGuildAccess, requireModerator, async (req, res) => {
+ try {
+ const updates = req.body;
+
+ let settings = await LFGSettings.findOne({ guildId: req.guild.id });
+ if (!settings) {
+ settings = new LFGSettings({ guildId: req.guild.id });
+ }
+
+ // Update allowed fields
+ const allowedFields = ['enabled', 'channelId', 'autoDeleteAfter', 'allowedGames', 'maxPostsPerUser'];
+ allowedFields.forEach(field => {
+ if (updates[field] !== undefined) {
+ settings[field] = updates[field];
+ }
+ });
+
+ await settings.save();
+
+ res.json({
+ success: true,
+ message: 'LFG settings updated',
+ settings
+ });
+
+ } catch (error) {
+ req.client.logger.error('LFG settings update error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to update LFG settings'
+ });
+ }
+});
+
+/**
+ * DELETE /api/lfg/:guildId/posts/:postId
+ * Delete an LFG post
+ */
+router.delete('/:guildId/posts/:postId', validateGuildAccess, async (req, res) => {
+ try {
+ const { postId } = req.params;
+
+ const post = await LFGPost.findOne({
+ _id: postId,
+ guildId: req.guild.id
+ });
+
+ if (!post) {
+ return res.status(404).json({
+ error: 'Post Not Found',
+ message: 'LFG post does not exist'
+ });
+ }
+
+ // Users can delete their own posts, moderators can delete any
+ if (post.authorId !== req.auth.userId && !req.member.permissions.has('ModerateMembers')) {
+ return res.status(403).json({
+ error: 'Access Denied',
+ message: 'Can only delete your own posts'
+ });
+ }
+
+ await LFGPost.deleteOne({ _id: postId });
+
+ res.json({
+ success: true,
+ message: 'LFG post deleted successfully'
+ });
+
+ } catch (error) {
+ req.client.logger.error('Delete LFG post error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to delete LFG post'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/src/modules/web/routes/moderation.js b/src/modules/web/routes/moderation.js
new file mode 100644
index 0000000..93740df
--- /dev/null
+++ b/src/modules/web/routes/moderation.js
@@ -0,0 +1,448 @@
+/**
+ * Moderation Module Routes
+ */
+
+const express = require('express');
+const router = express.Router();
+const { verifyToken, validateGuildAccess, requireModerator, rateLimit } = require('../middleware/auth');
+const ModLog = require('../../../schemas/ModLog');
+const UserNotes = require('../../../schemas/UserNotes');
+const PunishmentLog = require('../../../schemas/PunishmentLog');
+
+// Apply authentication and rate limiting to all routes
+router.use(verifyToken);
+router.use(rateLimit(60000, 30)); // 30 requests per minute
+
+/**
+ * GET /api/moderation/:guildId/logs
+ * Get moderation logs with pagination
+ */
+router.get('/:guildId/logs', validateGuildAccess, requireModerator, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = Math.min(parseInt(req.query.limit) || 20, 100);
+ const type = req.query.type; // Optional filter by action type
+ const userId = req.query.userId; // Optional filter by user
+
+ const query = { guildId: req.guild.id };
+ if (type) query.action = type;
+ if (userId) query.userId = userId;
+
+ const total = await ModLog.countDocuments(query);
+ const logs = await ModLog.find(query)
+ .sort({ createdAt: -1 })
+ .limit(limit)
+ .skip((page - 1) * limit)
+ .populate('userId', 'username discriminator avatar')
+ .populate('moderatorId', 'username discriminator avatar');
+
+ res.json({
+ success: true,
+ logs,
+ pagination: {
+ page,
+ limit,
+ total,
+ totalPages: Math.ceil(total / limit),
+ hasNext: page * limit < total,
+ hasPrev: page > 1
+ }
+ });
+
+ } catch (error) {
+ req.client.logger.error('Moderation logs fetch error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to fetch moderation logs'
+ });
+ }
+});
+
+/**
+ * POST /api/moderation/:guildId/kick
+ * Kick a member from the guild
+ */
+router.post('/:guildId/kick', validateGuildAccess, requireModerator, async (req, res) => {
+ try {
+ const { userId, reason } = req.body;
+
+ if (!userId) {
+ return res.status(400).json({
+ error: 'Bad Request',
+ message: 'User ID is required'
+ });
+ }
+
+ const member = req.guild.members.cache.get(userId);
+ if (!member) {
+ return res.status(404).json({
+ error: 'Member Not Found',
+ message: 'User is not a member of this guild'
+ });
+ }
+
+ // Check permissions
+ if (!member.kickable) {
+ return res.status(403).json({
+ error: 'Insufficient Permissions',
+ message: 'Cannot kick this member due to role hierarchy'
+ });
+ }
+
+ if (member.permissions.has('Administrator')) {
+ return res.status(403).json({
+ error: 'Access Denied',
+ message: 'Cannot kick administrators'
+ });
+ }
+
+ // Perform kick
+ await member.kick(reason || 'No reason provided');
+
+ // Log the action
+ await ModLog.create({
+ guildId: req.guild.id,
+ userId: userId,
+ moderatorId: req.member.id,
+ action: 'KICK',
+ reason: reason || 'No reason provided'
+ });
+
+ res.json({
+ success: true,
+ message: `Successfully kicked ${member.user.username}`,
+ action: {
+ type: 'kick',
+ target: {
+ id: member.user.id,
+ username: member.user.username,
+ discriminator: member.user.discriminator
+ },
+ moderator: {
+ id: req.member.id,
+ username: req.member.user.username
+ },
+ reason: reason || 'No reason provided'
+ }
+ });
+
+ } catch (error) {
+ req.client.logger.error('Kick member error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to kick member'
+ });
+ }
+});
+
+/**
+ * POST /api/moderation/:guildId/ban
+ * Ban a user from the guild
+ */
+router.post('/:guildId/ban', validateGuildAccess, requireModerator, async (req, res) => {
+ try {
+ const { userId, reason, deleteMessageDays = 0 } = req.body;
+
+ if (!userId) {
+ return res.status(400).json({
+ error: 'Bad Request',
+ message: 'User ID is required'
+ });
+ }
+
+ // Check if user is already banned
+ const existingBan = await req.guild.bans.fetch(userId).catch(() => null);
+ if (existingBan) {
+ return res.status(400).json({
+ error: 'Already Banned',
+ message: 'User is already banned from this guild'
+ });
+ }
+
+ const member = req.guild.members.cache.get(userId);
+
+ // If member exists, check permissions
+ if (member) {
+ if (!member.bannable) {
+ return res.status(403).json({
+ error: 'Insufficient Permissions',
+ message: 'Cannot ban this member due to role hierarchy'
+ });
+ }
+
+ if (member.permissions.has('Administrator')) {
+ return res.status(403).json({
+ error: 'Access Denied',
+ message: 'Cannot ban administrators'
+ });
+ }
+ }
+
+ // Perform ban
+ await req.guild.members.ban(userId, {
+ reason: reason || 'No reason provided',
+ deleteMessageDays: Math.min(deleteMessageDays, 7)
+ });
+
+ // Log the action
+ await ModLog.create({
+ guildId: req.guild.id,
+ userId: userId,
+ moderatorId: req.member.id,
+ action: 'BAN',
+ reason: reason || 'No reason provided'
+ });
+
+ const targetUser = member ? member.user : await req.client.users.fetch(userId).catch(() => null);
+
+ res.json({
+ success: true,
+ message: `Successfully banned ${targetUser?.username || 'user'}`,
+ action: {
+ type: 'ban',
+ target: {
+ id: userId,
+ username: targetUser?.username || 'Unknown',
+ discriminator: targetUser?.discriminator || '0000'
+ },
+ moderator: {
+ id: req.member.id,
+ username: req.member.user.username
+ },
+ reason: reason || 'No reason provided'
+ }
+ });
+
+ } catch (error) {
+ req.client.logger.error('Ban user error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to ban user'
+ });
+ }
+});
+
+/**
+ * DELETE /api/moderation/:guildId/ban/:userId
+ * Unban a user from the guild
+ */
+router.delete('/:guildId/ban/:userId', validateGuildAccess, requireModerator, async (req, res) => {
+ try {
+ const { userId } = req.params;
+ const { reason } = req.body;
+
+ // Check if user is banned
+ const ban = await req.guild.bans.fetch(userId).catch(() => null);
+ if (!ban) {
+ return res.status(404).json({
+ error: 'Not Banned',
+ message: 'User is not banned from this guild'
+ });
+ }
+
+ // Perform unban
+ await req.guild.members.unban(userId, reason || 'No reason provided');
+
+ // Log the action
+ await ModLog.create({
+ guildId: req.guild.id,
+ userId: userId,
+ moderatorId: req.member.id,
+ action: 'UNBAN',
+ reason: reason || 'No reason provided'
+ });
+
+ res.json({
+ success: true,
+ message: `Successfully unbanned ${ban.user.username}`,
+ action: {
+ type: 'unban',
+ target: {
+ id: ban.user.id,
+ username: ban.user.username,
+ discriminator: ban.user.discriminator
+ },
+ moderator: {
+ id: req.member.id,
+ username: req.member.user.username
+ },
+ reason: reason || 'No reason provided'
+ }
+ });
+
+ } catch (error) {
+ req.client.logger.error('Unban user error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to unban user'
+ });
+ }
+});
+
+/**
+ * POST /api/moderation/:guildId/timeout
+ * Timeout a member
+ */
+router.post('/:guildId/timeout', validateGuildAccess, requireModerator, async (req, res) => {
+ try {
+ const { userId, duration, reason } = req.body;
+
+ if (!userId || !duration) {
+ return res.status(400).json({
+ error: 'Bad Request',
+ message: 'User ID and duration are required'
+ });
+ }
+
+ const member = req.guild.members.cache.get(userId);
+ if (!member) {
+ return res.status(404).json({
+ error: 'Member Not Found',
+ message: 'User is not a member of this guild'
+ });
+ }
+
+ // Check permissions
+ if (!member.moderatable) {
+ return res.status(403).json({
+ error: 'Insufficient Permissions',
+ message: 'Cannot timeout this member due to role hierarchy'
+ });
+ }
+
+ if (member.permissions.has('Administrator')) {
+ return res.status(403).json({
+ error: 'Access Denied',
+ message: 'Cannot timeout administrators'
+ });
+ }
+
+ // Validate duration (max 28 days)
+ const maxDuration = 28 * 24 * 60 * 60 * 1000; // 28 days in milliseconds
+ const timeoutDuration = Math.min(parseInt(duration), maxDuration);
+
+ // Perform timeout
+ await member.timeout(timeoutDuration, reason || 'No reason provided');
+
+ // Log the action
+ await ModLog.create({
+ guildId: req.guild.id,
+ userId: userId,
+ moderatorId: req.member.id,
+ action: 'TIMEOUT',
+ reason: reason || 'No reason provided',
+ duration: timeoutDuration
+ });
+
+ res.json({
+ success: true,
+ message: `Successfully timed out ${member.user.username} for ${Math.floor(timeoutDuration / (60 * 1000))} minutes`,
+ action: {
+ type: 'timeout',
+ target: {
+ id: member.user.id,
+ username: member.user.username,
+ discriminator: member.user.discriminator
+ },
+ moderator: {
+ id: req.member.id,
+ username: req.member.user.username
+ },
+ reason: reason || 'No reason provided',
+ duration: timeoutDuration
+ }
+ });
+
+ } catch (error) {
+ req.client.logger.error('Timeout member error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to timeout member'
+ });
+ }
+});
+
+/**
+ * GET /api/moderation/:guildId/user/:userId/notes
+ * Get user notes
+ */
+router.get('/:guildId/user/:userId/notes', validateGuildAccess, requireModerator, async (req, res) => {
+ try {
+ const { userId } = req.params;
+
+ const userNotes = await UserNotes.findOne({
+ guildId: req.guild.id,
+ userId: userId
+ });
+
+ res.json({
+ success: true,
+ notes: userNotes?.notes || []
+ });
+
+ } catch (error) {
+ req.client.logger.error('User notes fetch error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to fetch user notes'
+ });
+ }
+});
+
+/**
+ * POST /api/moderation/:guildId/user/:userId/notes
+ * Add a note to a user
+ */
+router.post('/:guildId/user/:userId/notes', validateGuildAccess, requireModerator, async (req, res) => {
+ try {
+ const { userId } = req.params;
+ const { note } = req.body;
+
+ if (!note || note.trim().length === 0) {
+ return res.status(400).json({
+ error: 'Bad Request',
+ message: 'Note content is required'
+ });
+ }
+
+ const newNote = {
+ id: Date.now().toString(),
+ moderatorId: req.member.id,
+ moderatorTag: req.member.user.tag,
+ note: note.trim(),
+ createdAt: new Date()
+ };
+
+ let userNotes = await UserNotes.findOne({
+ guildId: req.guild.id,
+ userId: userId
+ });
+
+ if (!userNotes) {
+ userNotes = new UserNotes({
+ guildId: req.guild.id,
+ userId: userId,
+ notes: [newNote]
+ });
+ } else {
+ userNotes.notes.push(newNote);
+ }
+
+ await userNotes.save();
+
+ res.json({
+ success: true,
+ message: 'Note added successfully',
+ note: newNote
+ });
+
+ } catch (error) {
+ req.client.logger.error('Add user note error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to add user note'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/src/modules/web/routes/music.js b/src/modules/web/routes/music.js
new file mode 100644
index 0000000..25bb55b
--- /dev/null
+++ b/src/modules/web/routes/music.js
@@ -0,0 +1,420 @@
+/**
+ * Music Module Routes
+ */
+
+const express = require('express');
+const router = express.Router();
+const { verifyToken, validateGuildAccess, requireDJ, rateLimit } = require('../middleware/auth');
+
+// Apply authentication and rate limiting to all routes
+router.use(verifyToken);
+router.use(rateLimit(60000, 30)); // 30 requests per minute
+
+/**
+ * GET /api/music/:guildId/player
+ * Get current player status and queue
+ */
+router.get('/:guildId/player', validateGuildAccess, (req, res) => {
+ try {
+ const player = req.client.manager?.players.cache.get(req.guild.id);
+
+ if (!player) {
+ return res.json({
+ success: true,
+ player: null,
+ queue: [],
+ status: 'not_playing'
+ });
+ }
+
+ const currentTrack = player.current;
+ const queue = player.queue.map(track => ({
+ title: track.title,
+ author: track.author,
+ duration: track.duration,
+ uri: track.uri,
+ requester: track.requester?.id || null,
+ thumbnail: track.thumbnail
+ }));
+
+ res.json({
+ success: true,
+ player: {
+ guildId: player.guildId,
+ voiceChannelId: player.voiceChannelId,
+ textChannelId: player.textChannelId,
+ connected: player.connected,
+ playing: player.playing,
+ paused: player.paused,
+ volume: player.volume,
+ position: player.position,
+ repeatMode: player.repeatMode,
+ shuffled: player.shuffled
+ },
+ currentTrack: currentTrack ? {
+ title: currentTrack.title,
+ author: currentTrack.author,
+ duration: currentTrack.duration,
+ uri: currentTrack.uri,
+ requester: currentTrack.requester?.id || null,
+ thumbnail: currentTrack.thumbnail,
+ position: player.position
+ } : null,
+ queue,
+ status: player.playing ? 'playing' : player.paused ? 'paused' : 'stopped'
+ });
+
+ } catch (error) {
+ req.client.logger.error('Music player fetch error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to fetch player status'
+ });
+ }
+});
+
+/**
+ * POST /api/music/:guildId/play
+ * Add a track to queue or start playing
+ */
+router.post('/:guildId/play', validateGuildAccess, requireDJ, async (req, res) => {
+ try {
+ const { query, voiceChannelId } = req.body;
+
+ if (!query) {
+ return res.status(400).json({
+ error: 'Bad Request',
+ message: 'Search query is required'
+ });
+ }
+
+ if (!voiceChannelId) {
+ return res.status(400).json({
+ error: 'Bad Request',
+ message: 'Voice channel ID is required'
+ });
+ }
+
+ // Validate voice channel
+ const voiceChannel = req.guild.channels.cache.get(voiceChannelId);
+ if (!voiceChannel || voiceChannel.type !== 2) {
+ return res.status(400).json({
+ error: 'Invalid Channel',
+ message: 'Specified voice channel not found'
+ });
+ }
+
+ // Check if manager exists
+ if (!req.client.manager) {
+ return res.status(503).json({
+ error: 'Service Unavailable',
+ message: 'Music service is not available'
+ });
+ }
+
+ // Get or create player
+ let player = req.client.manager.players.cache.get(req.guild.id);
+ if (!player) {
+ player = req.client.manager.createPlayer({
+ guildId: req.guild.id,
+ voiceChannelId: voiceChannelId,
+ textChannelId: req.body.textChannelId || null
+ });
+ }
+
+ // Search for tracks
+ const searchResult = await req.client.manager.search(query, req.member.user);
+
+ if (!searchResult || !searchResult.tracks.length) {
+ return res.status(404).json({
+ error: 'Not Found',
+ message: 'No tracks found for the search query'
+ });
+ }
+
+ // Add track(s) to queue
+ const tracksAdded = searchResult.loadType === 'PLAYLIST_LOADED'
+ ? searchResult.tracks
+ : [searchResult.tracks[0]];
+
+ for (const track of tracksAdded) {
+ track.requester = req.member.user;
+ player.queue.add(track);
+ }
+
+ // Connect and play if not already playing
+ if (!player.connected) {
+ await player.connect();
+ }
+
+ if (!player.playing && !player.paused) {
+ await player.play();
+ }
+
+ res.json({
+ success: true,
+ message: tracksAdded.length === 1
+ ? `Added **${tracksAdded[0].title}** to queue`
+ : `Added **${tracksAdded.length}** tracks to queue`,
+ tracksAdded: tracksAdded.map(track => ({
+ title: track.title,
+ author: track.author,
+ duration: track.duration,
+ uri: track.uri,
+ thumbnail: track.thumbnail
+ })),
+ queueLength: player.queue.size
+ });
+
+ } catch (error) {
+ req.client.logger.error('Music play error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to play music'
+ });
+ }
+});
+
+/**
+ * POST /api/music/:guildId/pause
+ * Pause/resume playback
+ */
+router.post('/:guildId/pause', validateGuildAccess, requireDJ, (req, res) => {
+ try {
+ const player = req.client.manager?.players.cache.get(req.guild.id);
+
+ if (!player || !player.current) {
+ return res.status(400).json({
+ error: 'No Active Player',
+ message: 'No music is currently playing'
+ });
+ }
+
+ if (player.paused) {
+ player.resume();
+ res.json({
+ success: true,
+ message: 'Music resumed',
+ status: 'playing'
+ });
+ } else {
+ player.pause();
+ res.json({
+ success: true,
+ message: 'Music paused',
+ status: 'paused'
+ });
+ }
+
+ } catch (error) {
+ req.client.logger.error('Music pause error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to pause/resume music'
+ });
+ }
+});
+
+/**
+ * POST /api/music/:guildId/skip
+ * Skip current track
+ */
+router.post('/:guildId/skip', validateGuildAccess, requireDJ, (req, res) => {
+ try {
+ const player = req.client.manager?.players.cache.get(req.guild.id);
+
+ if (!player || !player.current) {
+ return res.status(400).json({
+ error: 'No Active Player',
+ message: 'No music is currently playing'
+ });
+ }
+
+ const skippedTrack = player.current.title;
+ player.skip();
+
+ res.json({
+ success: true,
+ message: `Skipped **${skippedTrack}**`,
+ queueLength: player.queue.size
+ });
+
+ } catch (error) {
+ req.client.logger.error('Music skip error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to skip track'
+ });
+ }
+});
+
+/**
+ * POST /api/music/:guildId/stop
+ * Stop playback and clear queue
+ */
+router.post('/:guildId/stop', validateGuildAccess, requireDJ, (req, res) => {
+ try {
+ const player = req.client.manager?.players.cache.get(req.guild.id);
+
+ if (!player) {
+ return res.status(400).json({
+ error: 'No Active Player',
+ message: 'No music player is active'
+ });
+ }
+
+ player.destroy();
+
+ res.json({
+ success: true,
+ message: 'Music stopped and queue cleared'
+ });
+
+ } catch (error) {
+ req.client.logger.error('Music stop error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to stop music'
+ });
+ }
+});
+
+/**
+ * POST /api/music/:guildId/volume
+ * Set playback volume
+ */
+router.post('/:guildId/volume', validateGuildAccess, requireDJ, (req, res) => {
+ try {
+ const { volume } = req.body;
+
+ if (volume === undefined || volume < 0 || volume > 150) {
+ return res.status(400).json({
+ error: 'Bad Request',
+ message: 'Volume must be between 0 and 150'
+ });
+ }
+
+ const player = req.client.manager?.players.cache.get(req.guild.id);
+
+ if (!player) {
+ return res.status(400).json({
+ error: 'No Active Player',
+ message: 'No music player is active'
+ });
+ }
+
+ player.setVolume(volume);
+
+ res.json({
+ success: true,
+ message: `Volume set to ${volume}%`,
+ volume
+ });
+
+ } catch (error) {
+ req.client.logger.error('Music volume error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to set volume'
+ });
+ }
+});
+
+/**
+ * POST /api/music/:guildId/seek
+ * Seek to position in current track
+ */
+router.post('/:guildId/seek', validateGuildAccess, requireDJ, (req, res) => {
+ try {
+ const { position } = req.body;
+
+ if (position === undefined || position < 0) {
+ return res.status(400).json({
+ error: 'Bad Request',
+ message: 'Position must be a positive number in milliseconds'
+ });
+ }
+
+ const player = req.client.manager?.players.cache.get(req.guild.id);
+
+ if (!player || !player.current) {
+ return res.status(400).json({
+ error: 'No Active Player',
+ message: 'No music is currently playing'
+ });
+ }
+
+ if (position > player.current.duration) {
+ return res.status(400).json({
+ error: 'Invalid Position',
+ message: 'Position exceeds track duration'
+ });
+ }
+
+ player.seek(position);
+
+ res.json({
+ success: true,
+ message: `Seeked to ${Math.floor(position / 1000)}s`,
+ position
+ });
+
+ } catch (error) {
+ req.client.logger.error('Music seek error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to seek in track'
+ });
+ }
+});
+
+/**
+ * DELETE /api/music/:guildId/queue/:index
+ * Remove track from queue
+ */
+router.delete('/:guildId/queue/:index', validateGuildAccess, requireDJ, (req, res) => {
+ try {
+ const index = parseInt(req.params.index);
+
+ if (isNaN(index) || index < 0) {
+ return res.status(400).json({
+ error: 'Bad Request',
+ message: 'Invalid queue index'
+ });
+ }
+
+ const player = req.client.manager?.players.cache.get(req.guild.id);
+
+ if (!player) {
+ return res.status(400).json({
+ error: 'No Active Player',
+ message: 'No music player is active'
+ });
+ }
+
+ if (index >= player.queue.size) {
+ return res.status(400).json({
+ error: 'Invalid Index',
+ message: 'Queue index out of range'
+ });
+ }
+
+ const removedTrack = player.queue[index];
+ player.queue.splice(index, 1);
+
+ res.json({
+ success: true,
+ message: `Removed **${removedTrack.title}** from queue`,
+ queueLength: player.queue.size
+ });
+
+ } catch (error) {
+ req.client.logger.error('Queue remove error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to remove track from queue'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/src/modules/web/routes/reminders.js b/src/modules/web/routes/reminders.js
new file mode 100644
index 0000000..4ec3865
--- /dev/null
+++ b/src/modules/web/routes/reminders.js
@@ -0,0 +1,170 @@
+/**
+ * Reminders Routes
+ */
+
+const express = require('express');
+const router = express.Router();
+const { verifyToken, validateGuildAccess, rateLimit } = require('../middleware/auth');
+const Reminder = require('../../../schemas/Reminder');
+
+// Apply authentication and rate limiting to all routes
+router.use(verifyToken);
+router.use(rateLimit(60000, 50)); // 50 requests per minute
+
+/**
+ * GET /api/reminders/:guildId
+ * Get reminders for the guild
+ */
+router.get('/:guildId', validateGuildAccess, async (req, res) => {
+ try {
+ const userId = req.query.userId; // Optional filter by user
+ const page = parseInt(req.query.page) || 1;
+ const limit = Math.min(parseInt(req.query.limit) || 20, 100);
+
+ const query = { guildId: req.guild.id };
+
+ // Users can only see their own reminders unless they're moderators
+ if (userId) {
+ if (userId !== req.auth.userId && !req.member.permissions.has('ModerateMembers')) {
+ return res.status(403).json({
+ error: 'Access Denied',
+ message: 'Can only view your own reminders'
+ });
+ }
+ query.userId = userId;
+ } else if (!req.member.permissions.has('ModerateMembers')) {
+ // Regular users can only see their own reminders
+ query.userId = req.auth.userId;
+ }
+
+ const total = await Reminder.countDocuments(query);
+ const reminders = await Reminder.find(query)
+ .sort({ reminderTime: 1 })
+ .limit(limit)
+ .skip((page - 1) * limit);
+
+ res.json({
+ success: true,
+ reminders,
+ pagination: {
+ page,
+ limit,
+ total,
+ totalPages: Math.ceil(total / limit)
+ }
+ });
+
+ } catch (error) {
+ req.client.logger.error('Reminders fetch error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to fetch reminders'
+ });
+ }
+});
+
+/**
+ * POST /api/reminders/:guildId
+ * Create a new reminder
+ */
+router.post('/:guildId', validateGuildAccess, async (req, res) => {
+ try {
+ const { message, reminderTime, channelId } = req.body;
+
+ if (!message || !reminderTime) {
+ return res.status(400).json({
+ error: 'Bad Request',
+ message: 'Message and reminder time are required'
+ });
+ }
+
+ const reminderDate = new Date(reminderTime);
+ if (isNaN(reminderDate.getTime()) || reminderDate <= new Date()) {
+ return res.status(400).json({
+ error: 'Invalid Time',
+ message: 'Reminder time must be a valid future date'
+ });
+ }
+
+ // Validate channel if provided
+ if (channelId) {
+ const channel = req.guild.channels.cache.get(channelId);
+ if (!channel) {
+ return res.status(404).json({
+ error: 'Channel Not Found',
+ message: 'Specified channel does not exist'
+ });
+ }
+ }
+
+ const reminder = new Reminder({
+ guildId: req.guild.id,
+ userId: req.auth.userId,
+ channelId: channelId || null,
+ message: message.trim(),
+ reminderTime: reminderDate,
+ createdAt: new Date()
+ });
+
+ await reminder.save();
+
+ res.json({
+ success: true,
+ message: 'Reminder created successfully',
+ reminder
+ });
+
+ } catch (error) {
+ req.client.logger.error('Create reminder error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to create reminder'
+ });
+ }
+});
+
+/**
+ * DELETE /api/reminders/:guildId/:reminderId
+ * Delete a reminder
+ */
+router.delete('/:guildId/:reminderId', validateGuildAccess, async (req, res) => {
+ try {
+ const { reminderId } = req.params;
+
+ const reminder = await Reminder.findOne({
+ _id: reminderId,
+ guildId: req.guild.id
+ });
+
+ if (!reminder) {
+ return res.status(404).json({
+ error: 'Reminder Not Found',
+ message: 'Reminder does not exist'
+ });
+ }
+
+ // Users can only delete their own reminders unless they're moderators
+ if (reminder.userId !== req.auth.userId && !req.member.permissions.has('ModerateMembers')) {
+ return res.status(403).json({
+ error: 'Access Denied',
+ message: 'Can only delete your own reminders'
+ });
+ }
+
+ await Reminder.deleteOne({ _id: reminderId });
+
+ res.json({
+ success: true,
+ message: 'Reminder deleted successfully'
+ });
+
+ } catch (error) {
+ req.client.logger.error('Delete reminder error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to delete reminder'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/src/modules/web/routes/roles.js b/src/modules/web/routes/roles.js
new file mode 100644
index 0000000..18d75e3
--- /dev/null
+++ b/src/modules/web/routes/roles.js
@@ -0,0 +1,244 @@
+/**
+ * Role Management Routes (SelfRole & AutoRole)
+ */
+
+const express = require('express');
+const router = express.Router();
+const { verifyToken, validateGuildAccess, requireAdmin, rateLimit } = require('../middleware/auth');
+const SelfRole = require('../../../schemas/SelfRole');
+
+// Apply authentication and rate limiting to all routes
+router.use(verifyToken);
+router.use(rateLimit(60000, 50)); // 50 requests per minute
+
+/**
+ * GET /api/roles/:guildId/selfroles
+ * Get all self-assignable roles
+ */
+router.get('/:guildId/selfroles', validateGuildAccess, async (req, res) => {
+ try {
+ const selfRoles = await SelfRole.find({ guildId: req.guild.id });
+
+ // Enrich with Discord role data
+ const enrichedRoles = selfRoles.map(selfRole => {
+ const role = req.guild.roles.cache.get(selfRole.roleId);
+ return {
+ ...selfRole.toObject(),
+ exists: !!role,
+ role: role ? {
+ name: role.name,
+ color: role.hexColor,
+ position: role.position,
+ memberCount: role.members.size
+ } : null
+ };
+ });
+
+ res.json({
+ success: true,
+ selfRoles: enrichedRoles
+ });
+
+ } catch (error) {
+ req.client.logger.error('Self roles fetch error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to fetch self roles'
+ });
+ }
+});
+
+/**
+ * POST /api/roles/:guildId/selfroles
+ * Add a new self-assignable role
+ */
+router.post('/:guildId/selfroles', validateGuildAccess, requireAdmin, async (req, res) => {
+ try {
+ const { roleId, emoji, description } = req.body;
+
+ if (!roleId) {
+ return res.status(400).json({
+ error: 'Bad Request',
+ message: 'Role ID is required'
+ });
+ }
+
+ // Validate role exists
+ const role = req.guild.roles.cache.get(roleId);
+ if (!role) {
+ return res.status(404).json({
+ error: 'Role Not Found',
+ message: 'Specified role does not exist'
+ });
+ }
+
+ // Check if role is already a self role
+ const existing = await SelfRole.findOne({ guildId: req.guild.id, roleId });
+ if (existing) {
+ return res.status(400).json({
+ error: 'Already Exists',
+ message: 'Role is already a self-assignable role'
+ });
+ }
+
+ const selfRole = new SelfRole({
+ guildId: req.guild.id,
+ roleId,
+ emoji: emoji || null,
+ description: description || null
+ });
+
+ await selfRole.save();
+
+ res.json({
+ success: true,
+ message: 'Self role added successfully',
+ selfRole: {
+ ...selfRole.toObject(),
+ role: {
+ name: role.name,
+ color: role.hexColor,
+ position: role.position,
+ memberCount: role.members.size
+ }
+ }
+ });
+
+ } catch (error) {
+ req.client.logger.error('Add self role error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to add self role'
+ });
+ }
+});
+
+/**
+ * DELETE /api/roles/:guildId/selfroles/:roleId
+ * Remove a self-assignable role
+ */
+router.delete('/:guildId/selfroles/:roleId', validateGuildAccess, requireAdmin, async (req, res) => {
+ try {
+ const { roleId } = req.params;
+
+ const selfRole = await SelfRole.findOneAndDelete({
+ guildId: req.guild.id,
+ roleId
+ });
+
+ if (!selfRole) {
+ return res.status(404).json({
+ error: 'Not Found',
+ message: 'Self role does not exist'
+ });
+ }
+
+ res.json({
+ success: true,
+ message: 'Self role removed successfully'
+ });
+
+ } catch (error) {
+ req.client.logger.error('Remove self role error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to remove self role'
+ });
+ }
+});
+
+/**
+ * POST /api/roles/:guildId/assign/:userId/:roleId
+ * Assign a role to a user (admin only)
+ */
+router.post('/:guildId/assign/:userId/:roleId', validateGuildAccess, requireAdmin, async (req, res) => {
+ try {
+ const { userId, roleId } = req.params;
+
+ const member = req.guild.members.cache.get(userId);
+ if (!member) {
+ return res.status(404).json({
+ error: 'Member Not Found',
+ message: 'User is not a member of this guild'
+ });
+ }
+
+ const role = req.guild.roles.cache.get(roleId);
+ if (!role) {
+ return res.status(404).json({
+ error: 'Role Not Found',
+ message: 'Specified role does not exist'
+ });
+ }
+
+ if (member.roles.cache.has(roleId)) {
+ return res.status(400).json({
+ error: 'Already Has Role',
+ message: 'User already has this role'
+ });
+ }
+
+ await member.roles.add(role);
+
+ res.json({
+ success: true,
+ message: `Successfully assigned ${role.name} to ${member.displayName}`
+ });
+
+ } catch (error) {
+ req.client.logger.error('Assign role error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to assign role'
+ });
+ }
+});
+
+/**
+ * DELETE /api/roles/:guildId/assign/:userId/:roleId
+ * Remove a role from a user (admin only)
+ */
+router.delete('/:guildId/assign/:userId/:roleId', validateGuildAccess, requireAdmin, async (req, res) => {
+ try {
+ const { userId, roleId } = req.params;
+
+ const member = req.guild.members.cache.get(userId);
+ if (!member) {
+ return res.status(404).json({
+ error: 'Member Not Found',
+ message: 'User is not a member of this guild'
+ });
+ }
+
+ const role = req.guild.roles.cache.get(roleId);
+ if (!role) {
+ return res.status(404).json({
+ error: 'Role Not Found',
+ message: 'Specified role does not exist'
+ });
+ }
+
+ if (!member.roles.cache.has(roleId)) {
+ return res.status(400).json({
+ error: 'Does Not Have Role',
+ message: 'User does not have this role'
+ });
+ }
+
+ await member.roles.remove(role);
+
+ res.json({
+ success: true,
+ message: `Successfully removed ${role.name} from ${member.displayName}`
+ });
+
+ } catch (error) {
+ req.client.logger.error('Remove role error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to remove role'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/src/modules/web/routes/templates.js b/src/modules/web/routes/templates.js
new file mode 100644
index 0000000..4880900
--- /dev/null
+++ b/src/modules/web/routes/templates.js
@@ -0,0 +1,281 @@
+/**
+ * Template Management Routes
+ */
+
+const express = require('express');
+const router = express.Router();
+const { verifyToken, validateGuildAccess, requireAdmin, rateLimit } = require('../middleware/auth');
+const EmbedTemplate = require('../../../schemas/EmbedTemplate');
+
+// Apply authentication and rate limiting to all routes
+router.use(verifyToken);
+router.use(rateLimit(60000, 50)); // 50 requests per minute
+
+/**
+ * GET /api/templates/:guildId
+ * Get all embed templates for the guild
+ */
+router.get('/:guildId', validateGuildAccess, async (req, res) => {
+ try {
+ const templates = await EmbedTemplate.find({ guildId: req.guild.id })
+ .sort({ name: 1 });
+
+ res.json({
+ success: true,
+ templates
+ });
+
+ } catch (error) {
+ req.client.logger.error('Templates fetch error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to fetch templates'
+ });
+ }
+});
+
+/**
+ * GET /api/templates/:guildId/:templateId
+ * Get a specific template
+ */
+router.get('/:guildId/:templateId', validateGuildAccess, async (req, res) => {
+ try {
+ const { templateId } = req.params;
+
+ const template = await EmbedTemplate.findOne({
+ _id: templateId,
+ guildId: req.guild.id
+ });
+
+ if (!template) {
+ return res.status(404).json({
+ error: 'Template Not Found',
+ message: 'Template does not exist'
+ });
+ }
+
+ res.json({
+ success: true,
+ template
+ });
+
+ } catch (error) {
+ req.client.logger.error('Template fetch error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to fetch template'
+ });
+ }
+});
+
+/**
+ * POST /api/templates/:guildId
+ * Create a new embed template
+ */
+router.post('/:guildId', validateGuildAccess, requireAdmin, async (req, res) => {
+ try {
+ const { name, description, embedData } = req.body;
+
+ if (!name || !embedData) {
+ return res.status(400).json({
+ error: 'Bad Request',
+ message: 'Name and embed data are required'
+ });
+ }
+
+ // Check if template name already exists
+ const existing = await EmbedTemplate.findOne({
+ guildId: req.guild.id,
+ name: name.trim()
+ });
+
+ if (existing) {
+ return res.status(400).json({
+ error: 'Already Exists',
+ message: 'Template with this name already exists'
+ });
+ }
+
+ const template = new EmbedTemplate({
+ guildId: req.guild.id,
+ name: name.trim(),
+ description: description?.trim() || null,
+ embedData,
+ authorId: req.auth.userId,
+ createdAt: new Date()
+ });
+
+ await template.save();
+
+ res.json({
+ success: true,
+ message: 'Template created successfully',
+ template
+ });
+
+ } catch (error) {
+ req.client.logger.error('Create template error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to create template'
+ });
+ }
+});
+
+/**
+ * PUT /api/templates/:guildId/:templateId
+ * Update an embed template
+ */
+router.put('/:guildId/:templateId', validateGuildAccess, requireAdmin, async (req, res) => {
+ try {
+ const { templateId } = req.params;
+ const { name, description, embedData } = req.body;
+
+ const template = await EmbedTemplate.findOne({
+ _id: templateId,
+ guildId: req.guild.id
+ });
+
+ if (!template) {
+ return res.status(404).json({
+ error: 'Template Not Found',
+ message: 'Template does not exist'
+ });
+ }
+
+ // Check if new name conflicts with existing template
+ if (name && name.trim() !== template.name) {
+ const existing = await EmbedTemplate.findOne({
+ guildId: req.guild.id,
+ name: name.trim(),
+ _id: { $ne: templateId }
+ });
+
+ if (existing) {
+ return res.status(400).json({
+ error: 'Already Exists',
+ message: 'Template with this name already exists'
+ });
+ }
+ }
+
+ // Update fields
+ if (name) template.name = name.trim();
+ if (description !== undefined) template.description = description?.trim() || null;
+ if (embedData) template.embedData = embedData;
+ template.updatedAt = new Date();
+
+ await template.save();
+
+ res.json({
+ success: true,
+ message: 'Template updated successfully',
+ template
+ });
+
+ } catch (error) {
+ req.client.logger.error('Update template error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to update template'
+ });
+ }
+});
+
+/**
+ * DELETE /api/templates/:guildId/:templateId
+ * Delete an embed template
+ */
+router.delete('/:guildId/:templateId', validateGuildAccess, requireAdmin, async (req, res) => {
+ try {
+ const { templateId } = req.params;
+
+ const template = await EmbedTemplate.findOneAndDelete({
+ _id: templateId,
+ guildId: req.guild.id
+ });
+
+ if (!template) {
+ return res.status(404).json({
+ error: 'Template Not Found',
+ message: 'Template does not exist'
+ });
+ }
+
+ res.json({
+ success: true,
+ message: 'Template deleted successfully'
+ });
+
+ } catch (error) {
+ req.client.logger.error('Delete template error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to delete template'
+ });
+ }
+});
+
+/**
+ * POST /api/templates/:guildId/:templateId/send
+ * Send a template to a specific channel
+ */
+router.post('/:guildId/:templateId/send', validateGuildAccess, requireAdmin, async (req, res) => {
+ try {
+ const { templateId } = req.params;
+ const { channelId } = req.body;
+
+ if (!channelId) {
+ return res.status(400).json({
+ error: 'Bad Request',
+ message: 'Channel ID is required'
+ });
+ }
+
+ const template = await EmbedTemplate.findOne({
+ _id: templateId,
+ guildId: req.guild.id
+ });
+
+ if (!template) {
+ return res.status(404).json({
+ error: 'Template Not Found',
+ message: 'Template does not exist'
+ });
+ }
+
+ const channel = req.guild.channels.cache.get(channelId);
+ if (!channel) {
+ return res.status(404).json({
+ error: 'Channel Not Found',
+ message: 'Specified channel does not exist'
+ });
+ }
+
+ if (!channel.isTextBased()) {
+ return res.status(400).json({
+ error: 'Invalid Channel',
+ message: 'Channel must be a text channel'
+ });
+ }
+
+ // Send the embed
+ const message = await channel.send({ embeds: [template.embedData] });
+
+ res.json({
+ success: true,
+ message: 'Template sent successfully',
+ messageId: message.id,
+ channelId: channel.id
+ });
+
+ } catch (error) {
+ req.client.logger.error('Send template error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to send template'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/src/modules/web/routes/tempvc.js b/src/modules/web/routes/tempvc.js
new file mode 100644
index 0000000..a3b884f
--- /dev/null
+++ b/src/modules/web/routes/tempvc.js
@@ -0,0 +1,180 @@
+/**
+ * Temporary Voice Channels Routes
+ */
+
+const express = require('express');
+const router = express.Router();
+const { verifyToken, validateGuildAccess, requireAdmin, rateLimit } = require('../middleware/auth');
+const TempVCInstance = require('../../../schemas/TempVCInstance');
+const TempVCUserSettings = require('../../../schemas/TempVCUserSettings');
+
+// Apply authentication and rate limiting to all routes
+router.use(verifyToken);
+router.use(rateLimit(60000, 50)); // 50 requests per minute
+
+/**
+ * GET /api/tempvc/:guildId/instances
+ * Get all active temporary voice channels
+ */
+router.get('/:guildId/instances', validateGuildAccess, requireAdmin, async (req, res) => {
+ try {
+ const instances = await TempVCInstance.find({ guildId: req.guild.id });
+
+ // Add live member count from Discord
+ const enrichedInstances = instances.map(instance => {
+ const channel = req.guild.channels.cache.get(instance.channelId);
+ return {
+ ...instance.toObject(),
+ memberCount: channel ? channel.members.size : 0,
+ exists: !!channel
+ };
+ });
+
+ res.json({
+ success: true,
+ instances: enrichedInstances
+ });
+
+ } catch (error) {
+ req.client.logger.error('TempVC instances fetch error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to fetch TempVC instances'
+ });
+ }
+});
+
+/**
+ * GET /api/tempvc/:guildId/settings/:userId
+ * Get user's TempVC settings
+ */
+router.get('/:guildId/settings/:userId', validateGuildAccess, async (req, res) => {
+ try {
+ const { userId } = req.params;
+
+ // Users can only view their own settings unless admin
+ if (userId !== req.auth.userId && !req.member.permissions.has('Administrator')) {
+ return res.status(403).json({
+ error: 'Access Denied',
+ message: 'Can only access your own settings'
+ });
+ }
+
+ const settings = await TempVCUserSettings.findOne({
+ guildId: req.guild.id,
+ userId: userId
+ });
+
+ res.json({
+ success: true,
+ settings: settings?.defaultSettings || {}
+ });
+
+ } catch (error) {
+ req.client.logger.error('TempVC settings fetch error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to fetch TempVC settings'
+ });
+ }
+});
+
+/**
+ * PUT /api/tempvc/:guildId/settings/:userId
+ * Update user's TempVC settings
+ */
+router.put('/:guildId/settings/:userId', validateGuildAccess, async (req, res) => {
+ try {
+ const { userId } = req.params;
+ const updates = req.body;
+
+ // Users can only update their own settings unless admin
+ if (userId !== req.auth.userId && !req.member.permissions.has('Administrator')) {
+ return res.status(403).json({
+ error: 'Access Denied',
+ message: 'Can only update your own settings'
+ });
+ }
+
+ let settings = await TempVCUserSettings.findOne({
+ guildId: req.guild.id,
+ userId: userId
+ });
+
+ if (!settings) {
+ settings = new TempVCUserSettings({
+ guildId: req.guild.id,
+ userId: userId,
+ defaultSettings: {}
+ });
+ }
+
+ // Update allowed fields
+ const allowedFields = ['channelName', 'userLimit', 'bitrate', 'isPrivate', 'allowedUsers', 'blockedUsers'];
+ allowedFields.forEach(field => {
+ if (updates[field] !== undefined) {
+ settings.defaultSettings[field] = updates[field];
+ }
+ });
+
+ await settings.save();
+
+ res.json({
+ success: true,
+ message: 'TempVC settings updated',
+ settings: settings.defaultSettings
+ });
+
+ } catch (error) {
+ req.client.logger.error('TempVC settings update error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to update TempVC settings'
+ });
+ }
+});
+
+/**
+ * DELETE /api/tempvc/:guildId/instances/:channelId
+ * Delete a temporary voice channel (admin only)
+ */
+router.delete('/:guildId/instances/:channelId', validateGuildAccess, requireAdmin, async (req, res) => {
+ try {
+ const { channelId } = req.params;
+
+ const instance = await TempVCInstance.findOne({
+ guildId: req.guild.id,
+ channelId: channelId
+ });
+
+ if (!instance) {
+ return res.status(404).json({
+ error: 'Instance Not Found',
+ message: 'TempVC instance does not exist'
+ });
+ }
+
+ // Delete the Discord channel
+ const channel = req.guild.channels.cache.get(channelId);
+ if (channel) {
+ await channel.delete('Deleted via dashboard');
+ }
+
+ // Remove from database
+ await TempVCInstance.deleteOne({ _id: instance._id });
+
+ res.json({
+ success: true,
+ message: 'TempVC instance deleted successfully'
+ });
+
+ } catch (error) {
+ req.client.logger.error('TempVC delete error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to delete TempVC instance'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/src/modules/web/routes/tickets.js b/src/modules/web/routes/tickets.js
new file mode 100644
index 0000000..28d35b5
--- /dev/null
+++ b/src/modules/web/routes/tickets.js
@@ -0,0 +1,173 @@
+/**
+ * Ticket System Routes
+ */
+
+const express = require('express');
+const router = express.Router();
+const { verifyToken, validateGuildAccess, requireModerator, rateLimit } = require('../middleware/auth');
+const Ticket = require('../../../schemas/Ticket');
+const TicketConfig = require('../../../schemas/TicketConfig');
+
+// Apply authentication and rate limiting to all routes
+router.use(verifyToken);
+router.use(rateLimit(60000, 50)); // 50 requests per minute
+
+/**
+ * GET /api/tickets/:guildId
+ * Get all tickets for a guild
+ */
+router.get('/:guildId', validateGuildAccess, requireModerator, async (req, res) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = Math.min(parseInt(req.query.limit) || 20, 100);
+ const status = req.query.status; // open, closed, all
+
+ const query = { guildId: req.guild.id };
+ if (status && status !== 'all') {
+ query.status = status;
+ }
+
+ const total = await Ticket.countDocuments(query);
+ const tickets = await Ticket.find(query)
+ .sort({ createdAt: -1 })
+ .limit(limit)
+ .skip((page - 1) * limit);
+
+ res.json({
+ success: true,
+ tickets,
+ pagination: {
+ page,
+ limit,
+ total,
+ totalPages: Math.ceil(total / limit)
+ }
+ });
+
+ } catch (error) {
+ req.client.logger.error('Tickets fetch error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to fetch tickets'
+ });
+ }
+});
+
+/**
+ * GET /api/tickets/:guildId/config
+ * Get ticket system configuration
+ */
+router.get('/:guildId/config', validateGuildAccess, requireModerator, async (req, res) => {
+ try {
+ const config = await TicketConfig.findOne({ guildId: req.guild.id });
+
+ res.json({
+ success: true,
+ config: config || {
+ enabled: false,
+ categoryId: null,
+ supportRoles: [],
+ maxTicketsPerUser: 1,
+ autoClose: false,
+ autoCloseTime: 24
+ }
+ });
+
+ } catch (error) {
+ req.client.logger.error('Ticket config fetch error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to fetch ticket configuration'
+ });
+ }
+});
+
+/**
+ * PUT /api/tickets/:guildId/config
+ * Update ticket system configuration
+ */
+router.put('/:guildId/config', validateGuildAccess, requireModerator, async (req, res) => {
+ try {
+ const updates = req.body;
+
+ let config = await TicketConfig.findOne({ guildId: req.guild.id });
+ if (!config) {
+ config = new TicketConfig({ guildId: req.guild.id });
+ }
+
+ // Update allowed fields
+ const allowedFields = ['enabled', 'categoryId', 'supportRoles', 'maxTicketsPerUser', 'autoClose', 'autoCloseTime'];
+ allowedFields.forEach(field => {
+ if (updates[field] !== undefined) {
+ config[field] = updates[field];
+ }
+ });
+
+ await config.save();
+
+ res.json({
+ success: true,
+ message: 'Ticket configuration updated',
+ config
+ });
+
+ } catch (error) {
+ req.client.logger.error('Ticket config update error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to update ticket configuration'
+ });
+ }
+});
+
+/**
+ * POST /api/tickets/:guildId/:ticketId/close
+ * Close a ticket
+ */
+router.post('/:guildId/:ticketId/close', validateGuildAccess, requireModerator, async (req, res) => {
+ try {
+ const { ticketId } = req.params;
+ const { reason } = req.body;
+
+ const ticket = await Ticket.findOne({
+ _id: ticketId,
+ guildId: req.guild.id
+ });
+
+ if (!ticket) {
+ return res.status(404).json({
+ error: 'Ticket Not Found',
+ message: 'Ticket does not exist'
+ });
+ }
+
+ if (ticket.status === 'closed') {
+ return res.status(400).json({
+ error: 'Already Closed',
+ message: 'Ticket is already closed'
+ });
+ }
+
+ ticket.status = 'closed';
+ ticket.closedBy = req.member.id;
+ ticket.closedAt = new Date();
+ ticket.closeReason = reason || 'No reason provided';
+
+ await ticket.save();
+
+ res.json({
+ success: true,
+ message: 'Ticket closed successfully',
+ ticket
+ });
+
+ } catch (error) {
+ req.client.logger.error('Close ticket error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to close ticket'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/src/modules/web/routes/user.js b/src/modules/web/routes/user.js
new file mode 100644
index 0000000..af94b6b
--- /dev/null
+++ b/src/modules/web/routes/user.js
@@ -0,0 +1,126 @@
+/**
+ * User Management Routes
+ */
+
+const express = require('express');
+const router = express.Router();
+const { verifyToken, validateGuildAccess, requireModerator, rateLimit } = require('../middleware/auth');
+const User = require('../../../schemas/User');
+
+// Apply authentication and rate limiting to all routes
+router.use(verifyToken);
+router.use(rateLimit(60000, 50)); // 50 requests per minute
+
+/**
+ * GET /api/user/:guildId/:userId
+ * Get user information
+ */
+router.get('/:guildId/:userId', validateGuildAccess, async (req, res) => {
+ try {
+ const { userId } = req.params;
+
+ // Users can view their own info, moderators can view anyone's
+ if (userId !== req.auth.userId && !req.member.permissions.has('ModerateMembers')) {
+ return res.status(403).json({
+ error: 'Access Denied',
+ message: 'Insufficient permissions to view user information'
+ });
+ }
+
+ const member = req.guild.members.cache.get(userId);
+ if (!member) {
+ return res.status(404).json({
+ error: 'User Not Found',
+ message: 'User is not a member of this guild'
+ });
+ }
+
+ const userDoc = await User.findOne({ userId: userId });
+
+ res.json({
+ success: true,
+ user: {
+ id: member.user.id,
+ username: member.user.username,
+ discriminator: member.user.discriminator,
+ avatar: member.user.displayAvatarURL(),
+ displayName: member.displayName,
+ joinedAt: member.joinedAt,
+ premiumSince: member.premiumSince,
+ roles: member.roles.cache
+ .filter(role => role.id !== req.guild.id)
+ .map(role => ({
+ id: role.id,
+ name: role.name,
+ color: role.hexColor,
+ position: role.position
+ }))
+ .sort((a, b) => b.position - a.position),
+ permissions: {
+ administrator: member.permissions.has('Administrator'),
+ manageGuild: member.permissions.has('ManageGuild'),
+ moderateMembers: member.permissions.has('ModerateMembers'),
+ manageMessages: member.permissions.has('ManageMessages')
+ },
+ database: userDoc || null
+ }
+ });
+
+ } catch (error) {
+ req.client.logger.error('User info fetch error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to fetch user information'
+ });
+ }
+});
+
+/**
+ * GET /api/user/:guildId/search
+ * Search for users in the guild
+ */
+router.get('/:guildId/search', validateGuildAccess, requireModerator, (req, res) => {
+ try {
+ const query = req.query.q?.toLowerCase();
+ const limit = Math.min(parseInt(req.query.limit) || 10, 50);
+
+ if (!query || query.length < 2) {
+ return res.status(400).json({
+ error: 'Bad Request',
+ message: 'Search query must be at least 2 characters'
+ });
+ }
+
+ const members = Array.from(req.guild.members.cache.values())
+ .filter(member =>
+ member.user.username.toLowerCase().includes(query) ||
+ member.displayName.toLowerCase().includes(query) ||
+ member.user.id.includes(query)
+ )
+ .slice(0, limit)
+ .map(member => ({
+ id: member.user.id,
+ username: member.user.username,
+ discriminator: member.user.discriminator,
+ displayName: member.displayName,
+ avatar: member.user.displayAvatarURL(),
+ joinedAt: member.joinedAt
+ }));
+
+ res.json({
+ success: true,
+ users: members,
+ query,
+ count: members.length
+ });
+
+ } catch (error) {
+ req.client.logger.error('User search error:', error);
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: 'Failed to search users'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file