aboutsummaryrefslogtreecommitdiff
path: root/bot/src
diff options
context:
space:
mode:
Diffstat (limited to 'bot/src')
-rw-r--r--bot/src/api/routes/auth.js73
-rw-r--r--bot/src/api/routes/quotes.js9
-rw-r--r--bot/src/api/routes/settings.js9
-rw-r--r--bot/src/api/server.js9
-rw-r--r--bot/src/commands/settings.js107
5 files changed, 184 insertions, 23 deletions
diff --git a/bot/src/api/routes/auth.js b/bot/src/api/routes/auth.js
new file mode 100644
index 0000000..224a2d1
--- /dev/null
+++ b/bot/src/api/routes/auth.js
@@ -0,0 +1,73 @@
+import { Router } from 'express';
+import jwt from 'jsonwebtoken';
+import bcrypt from 'bcrypt';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+// Check if required environment variables are set
+const requiredEnvVars = ['JWT_SECRET', 'AUTH_USERNAME', 'AUTH_PASSWORD_HASH'];
+const missingVars = requiredEnvVars.filter(varName => !process.env[varName]);
+if (missingVars.length > 0) {
+ console.error(`Missing required environment variables: ${missingVars.join(', ')}`);
+ console.error('For AUTH_PASSWORD_HASH, run bcrypt with the round of 10');
+}
+
+export function authRouter() {
+ const router = Router();
+
+ // Login endpoint
+ router.post('/login', async (req, res) => {
+ try {
+ const { username, password } = req.body;
+
+ if (!username || !password) {
+ return res.status(400).json({ error: 'Username and password are required' });
+ }
+
+ // Check against environment variables
+ if (username !== process.env.API_USERNAME) {
+ return res.status(401).json({ error: 'Invalid credentials' });
+ }
+
+ // Verify password
+ const isPasswordValid = await bcrypt.compare(password, process.env.API_PASSWORD_HASH);
+ if (!isPasswordValid) {
+ return res.status(401).json({ error: 'Invalid credentials' });
+ }
+
+ // Generate JWT token
+ const token = jwt.sign(
+ { username: username },
+ process.env.JWT_SECRET,
+ { expiresIn: '12h' }
+ );
+
+ res.json({ token });
+ } catch (error) {
+ console.error('Login error:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+ });
+
+ return router;
+}
+
+// Middleware to verify JWT token
+export function verifyToken(req, res, next) {
+ const authHeader = req.headers.authorization;
+
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ return res.status(401).json({ error: 'No token provided' });
+ }
+
+ const token = authHeader.split(' ')[1];
+
+ try {
+ const decoded = jwt.verify(token, process.env.JWT_SECRET);
+ req.user = decoded;
+ next();
+ } catch {
+ return res.status(403).json({ error: 'Invalid or expired token' });
+ }
+}
diff --git a/bot/src/api/routes/quotes.js b/bot/src/api/routes/quotes.js
index d39bb28..7f9f255 100644
--- a/bot/src/api/routes/quotes.js
+++ b/bot/src/api/routes/quotes.js
@@ -1,9 +1,10 @@
import { Router } from 'express';
import { pendingQuote, quote as newQuote } from '../../models/quote.js';
+import { verifyToken } from './auth.js';
export const quoteRouter = Router();
-quoteRouter.get('/quotes/pending', async (req, res) => {
+quoteRouter.get('/quotes/pending', verifyToken, async (req, res) => {
try {
const quotes = await pendingQuote.findAll();
res.json(quotes);
@@ -13,7 +14,7 @@ quoteRouter.get('/quotes/pending', async (req, res) => {
}
});
-quoteRouter.post('/quotes/add', async (req, res) => {
+quoteRouter.post('/quotes/add', verifyToken, async (req, res) => {
const { author, authorImage, quote, year, submitterID } = req.body;
try {
await newQuote.create({
@@ -30,7 +31,7 @@ quoteRouter.post('/quotes/add', async (req, res) => {
}
});
-quoteRouter.post('/quotes/approve', async (req, res) => {
+quoteRouter.post('/quotes/approve', verifyToken, async (req, res) => {
const { id } = req.body;
try {
const quote = await pendingQuote.findByPk(id);
@@ -53,7 +54,7 @@ quoteRouter.post('/quotes/approve', async (req, res) => {
}
});
-quoteRouter.post('/quotes/reject', async (req, res) => {
+quoteRouter.post('/quotes/reject', verifyToken, async (req, res) => {
const { id } = req.body;
try {
const quote = await pendingQuote.findByPk(id);
diff --git a/bot/src/api/routes/settings.js b/bot/src/api/routes/settings.js
index ce28acd..bdef633 100644
--- a/bot/src/api/routes/settings.js
+++ b/bot/src/api/routes/settings.js
@@ -1,11 +1,12 @@
import { ChannelType } from 'discord.js';
import { Router } from 'express';
import { guildSettings } from '../../models/guild-settings.js';
+import { verifyToken } from './auth.js';
export function settingsRouter(client) {
const router = Router();
- router.get('/settings/guild/:id', async (req, res) => {
+ router.get('/settings/guild/:id', verifyToken, async (req, res) => {
try {
const settings = await guildSettings.findOne({ where: { guildID: req.params.id } });
@@ -19,7 +20,6 @@ export function settingsRouter(client) {
const channelInfo = {
name: channel.name,
id: channel.id,
- position: channel.position,
category: channel.parent ? channel.parent.name : 'No Category'
};
@@ -42,9 +42,10 @@ export function settingsRouter(client) {
}
});
- router.post('/settings/guild', async (req, res) => {
+ router.post('/settings/guild/:id', verifyToken, async (req, res) => {
try {
- const { guildID, ...newSettings } = req.body;
+ const guildID = req.params.id;
+ const { ...newSettings } = req.body;
const [updated] = await guildSettings.update(newSettings, { where: { guildID: guildID } });
if (updated) {
const updatedSettings = await guildSettings.findOne({ where: { guildID: guildID } });
diff --git a/bot/src/api/server.js b/bot/src/api/server.js
index 15211eb..9ad2026 100644
--- a/bot/src/api/server.js
+++ b/bot/src/api/server.js
@@ -6,6 +6,7 @@ import { readFileSync } from 'node:fs';
import { quoteRouter } from './routes/quotes.js';
import { settingsRouter } from './routes/settings.js';
+import { authRouter, verifyToken } from './routes/auth.js';
const app = express();
@@ -15,11 +16,13 @@ export const apiServer = (client) => {
app.use('/api', quoteRouter);
app.use('/api', settingsRouter(client));
+ app.use('/api', authRouter());
app.get('/api/version', (req, res) => {
const { version } = JSON.parse(readFileSync('./package.json', 'utf-8'));
res.json({
- version: version
+ api_version: '1.0',
+ ab_version: version
});
});
@@ -30,7 +33,7 @@ export const apiServer = (client) => {
});
});
- app.get('/api/servers', (req, res) => {
+ app.get('/api/servers', verifyToken, (req, res) => {
const guildsInfo = [];
if (client.guilds.cache.size === 0) {
@@ -52,7 +55,7 @@ export const apiServer = (client) => {
});
- app.post('/api/leave', (req, res) => {
+ app.post('/api/leave', verifyToken, (req, res) => {
const { id } = req.body;
let guild = client.guilds.cache.get(id);
diff --git a/bot/src/commands/settings.js b/bot/src/commands/settings.js
index 4f29e0f..5deb371 100644
--- a/bot/src/commands/settings.js
+++ b/bot/src/commands/settings.js
@@ -1,24 +1,107 @@
-import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, SlashCommandBuilder } from 'discord.js';
+import { PermissionFlagsBits, SlashCommandBuilder, MessageFlags, EmbedBuilder } from 'discord.js';
+import { guildSettings } from '../models/guild-settings.js';
import { abEmbedColour } from '../storage/consts.js';
export default {
data: new SlashCommandBuilder()
.setName('settings')
- .setDescription('Settings for AleeBot.'),
+ .setDescription('Settings for AleeBot.')
+ .setContexts(0)
+ .addSubcommand(subcommand =>
+ subcommand
+ .setName('set')
+ .setDescription('Sets the settings for this guild.')
+ .addChannelOption(option =>
+ option
+ .setName('log')
+ .setDescription('Log channel.'))
+ .addChannelOption(option =>
+ option
+ .setName('suggestion')
+ .setDescription('Suggestion channel.'))
+ .addChannelOption(option =>
+ option
+ .setName('qotd')
+ .setDescription('Quote of the Day channel.'))
+ .addBooleanOption(option =>
+ option
+ .setName('qotdtoggle')
+ .setDescription('Toggle Quote of the Day.'))
+ .addBooleanOption(option =>
+ option
+ .setName('llmtoggle')
+ .setDescription('Toggle LLM Chatbot.')))
+ .addSubcommand(subcommand =>
+ subcommand
+ .setName('clear')
+ .setDescription('Clears all settings for this guild.')),
async execute(interaction) {
- const settingEmbed = new EmbedBuilder()
- .setAuthor({ name: 'AleeBot Settings', iconURL: interaction.client.user.avatarURL() })
- .setDescription(`To configure AleeBot, visit ${process.env.SETTINGS_URL}`)
+ if (!interaction.member.permissions.has(PermissionFlagsBits.ManageGuild) &&
+ !interaction.member.permissions.has(PermissionFlagsBits.Administrator) &&
+ interaction.user.id !== interaction.guild.ownerId) return await interaction.reply({ content: 'You do not have the permission to manage this guild.', flags: MessageFlags.Ephemeral });
+ const guildSetting = await guildSettings.findOne({ where: { guildID: interaction.guild.id } });
+ if (interaction.options.getSubcommand() === 'clear') {
+ await guildSettings.update({
+ logChannelID: null,
+ suggestionsChannelID: null,
+ qotdChannelID: null,
+ qotdToggle: null,
+ ollamaEnabled: null
+ }, { where: { guildID: interaction.guild.id } });
+ return await interaction.reply({ content: 'Cleared all settings for this guild.', flags: MessageFlags.Ephemeral });
+ }
+
+ const guildEmbed = new EmbedBuilder()
+ .setAuthor({ name: 'AleeBot Guild Settings', iconURL: interaction.client.user.avatarURL() })
+ .setDescription('Settings for this guild.')
.setColor(abEmbedColour);
- let settingButtons = new ActionRowBuilder()
- .addComponents(
- new ButtonBuilder()
- .setStyle(ButtonStyle.Link)
- .setLabel('Configure')
- .setURL(process.env.SETTINGS_URL)
+ if (!guildSetting) await guildSettings.create({ guildID: interaction.guild.id });
+
+
+ // Handle clearing settings
+ if (areAllSettingsEmpty(interaction)) {
+ guildEmbed.addFields(
+ { name: 'Logging', value: guildSetting?.logChannelID ? `<#${guildSetting.logChannelID}>` : 'N/A', inline: true },
+ { name: 'Suggestions', value: guildSetting?.suggestionsChannelID ? `<#${guildSetting.suggestionsChannelID}>` : 'N/A', inline: true },
+ { name: 'QOTD Channel', value: guildSetting?.qotdChannelID ? `<#${guildSetting.qotdChannelID}>` : 'N/A', inline: true },
+ { name: 'LLM Chatbot', value: guildSetting?.ollamaEnabled ? 'Enabled' : 'Disabled', inline: true },
+ { name: 'Quote of the Day', value: guildSetting?.qotdToggle ? 'Enabled' : 'Disabled', inline: true }
);
+ return await interaction.reply({ embeds: [guildEmbed], flags: MessageFlags.Ephemeral });
+ }
- return await interaction.reply({ embeds: [settingEmbed], components: [settingButtons] });
+ // Process each setting type
+ await updateChannelSetting(interaction, guildEmbed, 'log', 'logChannelID', 'Logging');
+ await updateChannelSetting(interaction, guildEmbed, 'suggestion', 'suggestionsChannelID', 'Suggestions');
+ await updateChannelSetting(interaction, guildEmbed, 'qotd', 'qotdChannelID', 'QOTD Channel');
+ await updateBooleanSetting(interaction, guildEmbed, 'qotdtoggle', 'qotdToggle', 'Quote of the Day');
+ await updateBooleanSetting(interaction, guildEmbed, 'llmtoggle', 'ollamaEnabled', 'LLM Chatbot');
+ return await interaction.reply({ embeds: [guildEmbed], flags: MessageFlags.Ephemeral });
}
};
+
+// Helper functions
+function areAllSettingsEmpty(interaction) {
+ return !interaction.options.getChannel('log') &&
+ !interaction.options.getChannel('suggestion') &&
+ !interaction.options.getChannel('qotd') &&
+ interaction.options.getBoolean('qotdtoggle') === null &&
+ interaction.options.getBoolean('llmtoggle') === null;
+}
+
+async function updateChannelSetting(interaction, embed, optionName, dbField, displayName) {
+ const channel = interaction.options.getChannel(optionName);
+ if (channel) {
+ embed.addFields({ name: displayName, value: `${channel}`, inline: true });
+ await guildSettings.update({ [dbField]: channel.id }, { where: { guildID: interaction.guild.id } });
+ }
+}
+
+async function updateBooleanSetting(interaction, embed, optionName, dbField, displayName) {
+ const value = interaction.options.getBoolean(optionName);
+ if (value !== null) {
+ embed.addFields({ name: displayName, value: value ? 'Enabled' : 'Disabled', inline: true });
+ await guildSettings.update({ [dbField]: value }, { where: { guildID: interaction.guild.id } });
+ }
+}