diff --git a/api/report-error.ts b/api/report-error.ts deleted file mode 100644 index d33b76b..0000000 --- a/api/report-error.ts +++ /dev/null @@ -1,203 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' - -/** - * Vercel Serverless Function for Error Reporting - * Receives error reports and forwards to Discord webhook - * Rate limited: 10 req/min/IP - */ - -interface ErrorReportPayload { - error: string - stack?: string - context: { - version: string - platform: string - arch: string - nodeVersion: string - timestamp: string - botMode?: string - } - additionalContext?: Record -} - -interface DiscordEmbed { - title: string - description: string - color: number - fields: Array<{ name: string; value: string; inline: boolean }> - timestamp: string - footer: { text: string } -} - -// Rate limiting configuration (in-memory, resets on cold start) -const rateLimitMap = new Map() -const RATE_LIMIT_WINDOW_MS = 60 * 1000 // 1 minute -const RATE_LIMIT_MAX_REQUESTS = 10 // 10 requests per minute per IP - -// Discord color constants -const DISCORD_COLOR_RED = 0xdc143c -const DISCORD_AVATAR_URL = 'https://raw.githubusercontent.com/LightZirconite/Microsoft-Rewards-Bot/refs/heads/main/assets/logo.png' - -/** - * Check if IP is rate limited - */ -function isRateLimited(ip: string, secret?: string): boolean { - // Bypass rate limit if valid secret provided - const validSecret = process.env.RATE_LIMIT_SECRET - if (secret && validSecret && secret === validSecret) { - return false - } - - const now = Date.now() - const entry = rateLimitMap.get(ip) - - if (!entry || now > entry.resetTime) { - // Reset or create new entry - rateLimitMap.set(ip, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS }) - return false - } - - if (entry.count >= RATE_LIMIT_MAX_REQUESTS) { - return true - } - - entry.count++ - return false -} - -/** - * Build Discord embed from error report - */ -function buildDiscordEmbed(payload: ErrorReportPayload): DiscordEmbed { - const { error, stack, context } = payload - - const osPlatform = (() => { - switch (context.platform) { - case 'win32': return '🪟 Windows' - case 'darwin': return '🍎 macOS' - case 'linux': return '🐧 Linux' - default: return context.platform - } - })() - - const embed: DiscordEmbed = { - title: '🐛 Community Error Report', - description: `\`\`\`js\n${error.slice(0, 700)}\n\`\`\``, - color: DISCORD_COLOR_RED, - fields: [ - { name: '📦 Version', value: context.version === 'unknown' ? '⚠️ Unknown' : `v${context.version}`, inline: true }, - { name: '🤖 Bot Mode', value: context.botMode || 'UNKNOWN', inline: true }, - { name: '💻 OS Platform', value: `${osPlatform} ${context.arch}`, inline: true }, - { name: '⚙️ Node.js', value: context.nodeVersion, inline: true }, - { name: '🕐 Timestamp', value: new Date(context.timestamp).toLocaleString('en-US', { timeZone: 'UTC', timeZoneName: 'short' }), inline: false } - ], - timestamp: context.timestamp, - footer: { text: 'Community Error Reporting • Vercel Serverless • Non-sensitive data only' } - } - - if (stack) { - const truncated = stack.slice(0, 900) - const wasTruncated = stack.length > 900 - embed.fields.push({ - name: '📋 Stack Trace' + (wasTruncated ? ' (truncated)' : ''), - value: `\`\`\`js\n${truncated}${wasTruncated ? '\n... (truncated for display)' : ''}\n\`\`\``, - inline: false - }) - } - - return embed -} - -/** - * Main handler for error reporting endpoint - */ -export default async function handler(req: VercelRequest, res: VercelResponse) { - // Enable CORS for all origins (error reporting should be accessible) - res.setHeader('Access-Control-Allow-Origin', '*') - res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS') - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Rate-Limit-Secret') - - // Handle preflight requests - if (req.method === 'OPTIONS') { - return res.status(200).end() - } - - // Only accept POST requests - if (req.method !== 'POST') { - return res.status(405).json({ error: 'Method not allowed', message: 'Only POST requests are accepted' }) - } - - try { - // Get client IP for rate limiting - const ip = (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() || - (req.headers['x-real-ip'] as string) || - 'unknown' - - // Get rate limit secret from headers - const rateLimitSecret = req.headers['x-rate-limit-secret'] as string | undefined - - // Check rate limit - if (isRateLimited(ip, rateLimitSecret)) { - return res.status(429).json({ - error: 'Rate limit exceeded', - message: 'Maximum 10 requests per minute per IP. Please try again later.' - }) - } - - // Validate Discord webhook URL is configured - const webhookUrl = process.env.DISCORD_ERROR_WEBHOOK_URL - if (!webhookUrl) { - console.error('[ErrorReporting] DISCORD_ERROR_WEBHOOK_URL not configured') - return res.status(500).json({ - error: 'Configuration error', - message: 'Discord webhook not configured. Please contact the administrator.' - }) - } - - // Validate request body - const payload = req.body as ErrorReportPayload - if (!payload || !payload.error || !payload.context) { - return res.status(400).json({ - error: 'Invalid payload', - message: 'Missing required fields: error, context' - }) - } - - // Build Discord embed - const embed = buildDiscordEmbed(payload) - - // Send to Discord webhook - const discordPayload = { - username: 'Microsoft-Rewards-Bot Error Reporter', - avatar_url: DISCORD_AVATAR_URL, - embeds: [embed] - } - - const response = await fetch(webhookUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(discordPayload) - }) - - if (!response.ok) { - console.error('[ErrorReporting] Discord webhook failed:', response.status, response.statusText) - return res.status(502).json({ - error: 'Discord webhook failed', - message: `HTTP ${response.status}: ${response.statusText}` - }) - } - - // Success - return res.status(200).json({ - success: true, - message: 'Error report sent successfully' - }) - - } catch (error) { - console.error('[ErrorReporting] Unexpected error:', error) - return res.status(500).json({ - error: 'Internal server error', - message: error instanceof Error ? error.message : 'Unknown error occurred' - }) - } -} diff --git a/src/dashboard/server.ts b/src/dashboard/server.ts index 9d25e8f..ebb6829 100644 --- a/src/dashboard/server.ts +++ b/src/dashboard/server.ts @@ -93,6 +93,56 @@ export class DashboardServer { res.json({ status: 'ok', uptime: process.uptime() }) }) + // Error reporting endpoint (community error collection) + this.app.post('/api/report-error', this.apiLimiter, async (req, res) => { + try { + const webhookUrl = process.env.DISCORD_ERROR_WEBHOOK_URL + if (!webhookUrl) { + dashLog('Error reporting: DISCORD_ERROR_WEBHOOK_URL not configured', 'warn') + return res.status(503).json({ error: 'Error reporting service unavailable' }) + } + + const payload = req.body + if (!payload?.error) { + return res.status(400).json({ error: 'Invalid payload' }) + } + + // Build Discord embed + const embed = { + title: '🔴 Bot Error Report', + description: `\`\`\`\n${String(payload.error).slice(0, 1900)}\n\`\`\``, + color: 0xdc143c, + fields: [ + { name: 'Version', value: String(payload.context?.version || 'unknown'), inline: true }, + { name: 'Platform', value: String(payload.context?.platform || 'unknown'), inline: true }, + { name: 'Node', value: String(payload.context?.nodeVersion || 'unknown'), inline: true } + ], + timestamp: new Date().toISOString(), + footer: { text: 'Community Error Reporting' } + } + + if (payload.stack) { + const stackLines = String(payload.stack).split('\n').slice(0, 15).join('\n') + embed.fields.push({ name: 'Stack Trace', value: `\`\`\`\n${stackLines.slice(0, 1000)}\n\`\`\``, inline: false }) + } + + // Send to Discord (use native axios, not AxiosClient) + const axios = (await import('axios')).default + await axios.post(webhookUrl, { + username: 'Microsoft Rewards Bot', + avatar_url: 'https://raw.githubusercontent.com/LightZirconite/Microsoft-Rewards-Bot/refs/heads/main/assets/logo.png', + embeds: [embed] + }, { timeout: 10000 }) + + dashLog('Error report sent to Discord', 'log') + return res.json({ success: true, message: 'Error report received' }) + + } catch (error) { + dashLog(`Error reporting failed: ${error instanceof Error ? error.message : String(error)}`, 'error') + return res.status(500).json({ error: 'Failed to send error report' }) + } + }) + // Serve dashboard UI this.app.get('/', this.dashboardLimiter, (_req, res) => { const indexPath = path.join(__dirname, '../../public/index.html') diff --git a/vercel.json b/vercel.json deleted file mode 100644 index c949ba6..0000000 --- a/vercel.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "functions": { - "api/**/*.ts": { - "runtime": "@vercel/node@3" - } - } -} \ No newline at end of file