diff --git a/aethex-bot/commands/verify-status.js b/aethex-bot/commands/verify-status.js new file mode 100644 index 0000000..b33c32b --- /dev/null +++ b/aethex-bot/commands/verify-status.js @@ -0,0 +1,105 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('verify-status') + .setDescription('Check your account verification status'), + + async execute(interaction, supabase, client) { + if (!supabase) { + return interaction.reply({ + content: 'This feature requires database configuration.', + ephemeral: true + }); + } + + await interaction.deferReply({ ephemeral: true }); + + try { + const { data: link } = await supabase + .from('discord_links') + .select('user_id, linked_at') + .eq('discord_id', interaction.user.id) + .maybeSingle(); + + const { data: pending } = await supabase + .from('discord_verifications') + .select('verification_code, expires_at') + .eq('discord_id', interaction.user.id) + .maybeSingle(); + + if (link) { + const linkedDate = new Date(link.linked_at).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }); + + const embed = new EmbedBuilder() + .setColor(0x00ff00) + .setTitle('✅ Verified Account') + .setDescription('Your Discord account is linked to AeThex.') + .addFields( + { name: '🔗 AeThex User ID', value: `\`${link.user_id}\``, inline: true }, + { name: '📅 Linked Since', value: linkedDate, inline: true } + ) + .setFooter({ text: 'Use /unlink to remove this connection' }); + + return interaction.editReply({ embeds: [embed] }); + } + + if (pending) { + const expiresAt = new Date(pending.expires_at); + const now = new Date(); + const expired = expiresAt < now; + + if (expired) { + const embed = new EmbedBuilder() + .setColor(0xff9500) + .setTitle('⏰ Verification Expired') + .setDescription('Your previous verification code has expired.') + .addFields( + { name: '💡 Next Step', value: 'Run `/verify` to generate a new code.' } + ); + + return interaction.editReply({ embeds: [embed] }); + } + + const remainingMs = expiresAt - now; + const remainingMins = Math.ceil(remainingMs / 60000); + + const embed = new EmbedBuilder() + .setColor(0x7289da) + .setTitle('⏳ Verification Pending') + .setDescription('You have a pending verification code.') + .addFields( + { name: '📝 Code', value: `\`${pending.verification_code}\``, inline: true }, + { name: '⏱️ Expires In', value: `${remainingMins} minute${remainingMins !== 1 ? 's' : ''}`, inline: true }, + { name: '💡 Next Step', value: 'Visit [aethex.dev/discord-verify](https://aethex.dev/discord-verify) to complete verification.' } + ); + + return interaction.editReply({ embeds: [embed] }); + } + + const embed = new EmbedBuilder() + .setColor(0xff5555) + .setTitle('❌ Not Verified') + .setDescription('Your Discord account is not linked to AeThex.') + .addFields( + { name: '💡 Get Started', value: 'Run `/verify` to link your account and unlock exclusive features!' } + ); + + return interaction.editReply({ embeds: [embed] }); + + } catch (error) { + console.error('Verify status error:', error); + + const embed = new EmbedBuilder() + .setColor(0xff0000) + .setTitle('❌ Error') + .setDescription('Failed to check verification status. Please try again.'); + + return interaction.editReply({ embeds: [embed] }); + } + } +}; diff --git a/aethex-bot/commands/verify.js b/aethex-bot/commands/verify.js index dc10ad5..3c07b69 100644 --- a/aethex-bot/commands/verify.js +++ b/aethex-bot/commands/verify.js @@ -7,6 +7,23 @@ const { } = require("discord.js"); const { syncRolesAcrossGuilds } = require("../utils/roleManager"); +async function checkAethexAvailability() { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + + const response = await fetch('https://aethex.dev/api/health', { + method: 'HEAD', + signal: controller.signal + }).catch(() => null); + + clearTimeout(timeoutId); + return response && response.ok; + } catch { + return false; + } +} + module.exports = { data: new SlashCommandBuilder() .setName("verify") @@ -19,6 +36,21 @@ module.exports = { await interaction.deferReply({ ephemeral: true }); try { + // Check if aethex.dev is reachable + const isAethexUp = await checkAethexAvailability(); + if (!isAethexUp) { + const embed = new EmbedBuilder() + .setColor(0xff9500) + .setTitle("⚠️ Service Temporarily Unavailable") + .setDescription("The AeThex verification service is currently unavailable.") + .addFields( + { name: "💡 What to do", value: "Please try again in a few minutes." }, + { name: "📊 Status", value: "Check [status.aethex.dev](https://status.aethex.dev) for updates." } + ); + + return await interaction.editReply({ embeds: [embed] }); + } + const { data: existingLink } = await supabase .from("discord_links") .select("*") @@ -43,13 +75,13 @@ module.exports = { .toUpperCase(); const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes - // Store verification code with Discord username - await supabase.from("discord_verifications").insert({ + // Store verification code with Discord username (upsert to prevent race condition) + await supabase.from("discord_verifications").upsert({ discord_id: interaction.user.id, verification_code: verificationCode, username: interaction.user.username, expires_at: expiresAt.toISOString(), - }); + }, { onConflict: 'discord_id' }); const verifyUrl = `https://aethex.dev/discord-verify?code=${verificationCode}`; diff --git a/aethex-bot/server/webServer.js b/aethex-bot/server/webServer.js index 72cb6af..bff73ad 100644 --- a/aethex-bot/server/webServer.js +++ b/aethex-bot/server/webServer.js @@ -329,6 +329,99 @@ function createWebServer(discordClient, supabase, options = {}) { // Route for specific user's profile app.get('/api/profile/:userId', (req, res) => handleProfileRequest(req, res, req.params.userId)); + // Verification callback endpoint - called by aethex.dev when user verifies + app.post('/api/verify-callback', async (req, res) => { + if (!supabase) { + return res.status(503).json({ error: 'Database not available' }); + } + + const { discord_id, user_id, secret } = req.body; + + // Verify the callback secret (should match what aethex.dev uses) + const expectedSecret = process.env.VERIFY_CALLBACK_SECRET; + if (expectedSecret && secret !== expectedSecret) { + return res.status(401).json({ error: 'Invalid callback secret' }); + } + + if (!discord_id || !user_id) { + return res.status(400).json({ error: 'Missing discord_id or user_id' }); + } + + try { + // Get server configs that have a verified role configured + const { data: configs } = await supabase + .from('server_config') + .select('guild_id, verified_role_id') + .not('verified_role_id', 'is', null); + + let rolesAssigned = 0; + + // Assign verified role in all guilds where the user is a member + for (const config of configs || []) { + if (!config.verified_role_id) continue; + + try { + const guild = client.guilds.cache.get(config.guild_id); + if (!guild) continue; + + const member = await guild.members.fetch(discord_id).catch(() => null); + if (!member) continue; + + const role = guild.roles.cache.get(config.verified_role_id); + if (!role) continue; + + await member.roles.add(role, 'Account verified via aethex.dev'); + rolesAssigned++; + console.log(`[Verify] Assigned verified role to ${member.user.tag} in ${guild.name}`); + } catch (err) { + console.error(`[Verify] Failed to assign role in guild ${config.guild_id}:`, err.message); + } + } + + // Clean up the verification code + await supabase.from('discord_verifications').delete().eq('discord_id', discord_id); + + res.json({ success: true, rolesAssigned }); + } catch (error) { + console.error('Verify callback error:', error); + res.status(500).json({ error: 'Failed to process verification callback' }); + } + }); + + // Verification status endpoint + app.get('/api/verify-status/:discordId', async (req, res) => { + if (!supabase) { + return res.status(503).json({ error: 'Database not available' }); + } + + const { discordId } = req.params; + + try { + const { data: link } = await supabase + .from('discord_links') + .select('user_id, linked_at') + .eq('discord_id', discordId) + .maybeSingle(); + + const { data: pending } = await supabase + .from('discord_verifications') + .select('verification_code, expires_at') + .eq('discord_id', discordId) + .maybeSingle(); + + res.json({ + verified: !!link, + userId: link?.user_id || null, + linkedAt: link?.linked_at || null, + pendingVerification: !!pending, + pendingExpires: pending?.expires_at || null + }); + } catch (error) { + console.error('Verify status error:', error); + res.status(500).json({ error: 'Failed to fetch verification status' }); + } + }); + app.get('/api/stats/:userId/:guildId', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' });