diff options
| author | Andrew Lee <andrew@alee14.me> | 2025-03-24 15:42:10 -0400 |
|---|---|---|
| committer | Andrew Lee <andrew@alee14.me> | 2025-03-24 15:42:10 -0400 |
| commit | ad768e2b25b58d62a44aa2daeb1429a651d488e5 (patch) | |
| tree | cadfaee0b8998c4d0d13a2a03bf18cc55e495264 | |
| parent | 0453bafa63ccd1057279a1be9286b3e7ebcb62d2 (diff) | |
| download | AleeBot-ad768e2b25b58d62a44aa2daeb1429a651d488e5.tar.gz AleeBot-ad768e2b25b58d62a44aa2daeb1429a651d488e5.tar.bz2 AleeBot-ad768e2b25b58d62a44aa2daeb1429a651d488e5.zip | |
Added JWT on API; Added back settings on Discord
| -rw-r--r-- | bot/.env.example | 9 | ||||
| -rw-r--r-- | bot/bun.lockb | bin | 124855 -> 134376 bytes | |||
| -rw-r--r-- | bot/package.json | 2 | ||||
| -rw-r--r-- | bot/src/api/routes/auth.js | 73 | ||||
| -rw-r--r-- | bot/src/api/routes/quotes.js | 9 | ||||
| -rw-r--r-- | bot/src/api/routes/settings.js | 9 | ||||
| -rw-r--r-- | bot/src/api/server.js | 9 | ||||
| -rw-r--r-- | bot/src/commands/settings.js | 107 | ||||
| -rw-r--r-- | web/src/app/page.js | 8 |
9 files changed, 201 insertions, 25 deletions
diff --git a/bot/.env.example b/bot/.env.example new file mode 100644 index 0000000..40766e0 --- /dev/null +++ b/bot/.env.example @@ -0,0 +1,9 @@ +TOKEN=[discord token] +PORT=3010 +CLIENT_ID=[Bot ID] +STATUS_CHANNEL_ID=[Channel ID] +OLLAMA_URL=[Ollama API URL] + +JWT_SECRET=[openssl rand -base64 32] +API_USERNAME=admin +API_PASSWORD_HASH=[bcrypt, 10] diff --git a/bot/bun.lockb b/bot/bun.lockb Binary files differindex a14de88..86b95df 100644 --- a/bot/bun.lockb +++ b/bot/bun.lockb diff --git a/bot/package.json b/bot/package.json index 5a0f631..4c937be 100644 --- a/bot/package.json +++ b/bot/package.json @@ -12,9 +12,11 @@ "lint": "eslint ." }, "dependencies": { + "bcrypt": "^5.1.1", "cors": "^2.8.5", "discord.js": "^14.18.0", "express": "^4.21.2", + "jsonwebtoken": "^9.0.2", "ollama": "^0.5.14", "sequelize": "^6.37.6", "sqlite3": "^5.1.7" 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 } }); + } +} diff --git a/web/src/app/page.js b/web/src/app/page.js index 65b3dda..76e5d27 100644 --- a/web/src/app/page.js +++ b/web/src/app/page.js @@ -1,5 +1,4 @@ import { redirect } from "next/navigation"; -import SignIn from "@/app/components/sign-in"; import { auth } from "@/lib/auth"; export default async function Home() { @@ -8,7 +7,12 @@ export default async function Home() { return ( <> <main className="flex justify-center items-center h-screen"> - <SignIn /> + <form className="flex flex-col gap-4 w-80"> + <input for='username' type='text' placeholder='Username' /> + <input for='password' type='password' placeholder='Password' /> + <input for='api' type='url' placeholder='API URL' /> + <input type="submit" value="Login" /> + </form> </main> </> ); |
