// src/pages/api/contribute/knowledgebase.ts import type { APIRoute } from 'astro'; import { withAPIAuth } from '../../../utils/auth.js'; import { apiResponse, apiError, apiServerError, handleAPIRequest } from '../../../utils/api.js'; import { GitContributionManager } from '../../../utils/gitContributions.js'; import { z } from 'zod'; export const prerender = false; const KnowledgebaseContributionSchema = z.object({ toolName: z.string().optional().nullable().transform(val => val || undefined), title: z.string().optional().nullable().transform(val => val || undefined), description: z.string().optional().nullable().transform(val => val || undefined), content: z.string().optional().nullable().transform(val => val || undefined), externalLink: z.string().url().optional().nullable().catch(undefined), difficulty: z.enum(['novice', 'beginner', 'intermediate', 'advanced', 'expert']).optional().nullable().catch(undefined), categories: z.string().transform(str => { try { return JSON.parse(str); } catch { return []; } }).pipe(z.array(z.string()).default([])), tags: z.string().transform(str => { try { return JSON.parse(str); } catch { return []; } }).pipe(z.array(z.string()).default([])), uploadedFiles: z.string().transform(str => { try { return JSON.parse(str); } catch { return []; } }).pipe(z.array(z.any()).default([])), reason: z.string().optional().nullable().transform(val => val || undefined), contact: z.string().optional().nullable().transform(val => val || undefined) }); interface KnowledgebaseContributionData { toolName?: string; title?: string; description?: string; content?: string; externalLink?: string; difficulty?: string; categories: string[]; tags: string[]; uploadedFiles: any[]; reason?: string; contact?: string; } const rateLimitStore = new Map(); const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour const RATE_LIMIT_MAX = 3; function checkRateLimit(userEmail: string): boolean { const now = Date.now(); const userLimit = rateLimitStore.get(userEmail); if (!userLimit || now > userLimit.resetTime) { rateLimitStore.set(userEmail, { count: 1, resetTime: now + RATE_LIMIT_WINDOW }); return true; } if (userLimit.count >= RATE_LIMIT_MAX) { return false; } userLimit.count++; return true; } function validateKnowledgebaseData(data: KnowledgebaseContributionData): { valid: boolean; errors?: string[] } { const hasContent = (data.content ?? '').trim().length > 0; const hasLink = (data.externalLink ?? '').trim().length > 0; const hasFiles = Array.isArray(data.uploadedFiles) && data.uploadedFiles.length > 0; const hasTitle = (data.title ?? '').trim().length > 0; const hasDescription = (data.description ?? '').trim().length > 0; if (!hasContent && !hasLink && !hasFiles && !hasTitle && !hasDescription) { return { valid: false, errors: ['Please provide at least a title, description, content, external link, or upload files'] }; } return { valid: true }; } export const POST: APIRoute = async ({ request }) => { return await handleAPIRequest(async () => { const authResult = await withAPIAuth(request, 'contributions'); if (authResult.authRequired && !authResult.authenticated) { return apiError.unauthorized(); } const userEmail = authResult.session?.email || 'anon@anon.anon'; if (!checkRateLimit(userEmail)) { return apiError.rateLimit('Rate limit exceeded. Please wait before submitting again.'); } let formData; try { formData = await request.formData(); } catch (error) { return apiError.badRequest('Invalid form data'); } const rawData = Object.fromEntries(formData); let validatedData; try { validatedData = KnowledgebaseContributionSchema.parse(rawData); } catch (error) { if (error instanceof z.ZodError) { const errorMessages = error.errors.map(err => `${err.path.join('.')}: ${err.message}` ); return apiError.validation('Validation failed', errorMessages); } return apiError.badRequest('Invalid request data'); } const contentValidation = validateKnowledgebaseData(validatedData); if (!contentValidation.valid) { return apiError.validation('Content validation failed', contentValidation.errors); } try { const gitManager = new GitContributionManager(); const result = await gitManager.submitKnowledgebaseContribution({ ...validatedData, submitter: userEmail }); if (result.success) { console.log(`[KB CONTRIBUTION] "${validatedData.title || 'Article'}" by ${userEmail} - Issue: ${result.issueUrl}`); return apiResponse.created({ success: true, message: result.message, issueUrl: result.issueUrl, issueNumber: result.issueNumber }); } else { console.error(`[KB CONTRIBUTION FAILED] "${validatedData.title || 'Article'}" by ${userEmail}: ${result.message}`); return apiServerError.internal(`Contribution failed: ${result.message}`); } } catch (error) { console.error(`[KB GIT ERROR] "${validatedData.title || 'Article'}" by ${userEmail}:`, error); const errorMessage = error instanceof Error ? error.message : 'Git operation failed'; return apiServerError.internal(`Submission failed: ${errorMessage}`); } }, 'Knowledgebase contribution processing failed'); };