// src/pages/api/contribute/tool.ts (UPDATED - Using consolidated API responses) import type { APIRoute } from 'astro'; import { withAPIAuth } from '../../../utils/auth.js'; import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js'; import { GitContributionManager, type ContributionData } from '../../../utils/gitContributions.js'; import { z } from 'zod'; export const prerender = false; // Enhanced tool schema for contributions (stricter validation) const ContributionToolSchema = z.object({ name: z.string().min(1, 'Tool name is required').max(100, 'Tool name too long'), icon: z.string().optional().nullable(), type: z.enum(['software', 'method', 'concept'], { errorMap: () => ({ message: 'Type must be software, method, or concept' }) }), description: z.string().min(10, 'Description must be at least 10 characters').max(1000, 'Description too long'), domains: z.array(z.string()).default([]), phases: z.array(z.string()).default([]), platforms: z.array(z.string()).default([]), skillLevel: z.enum(['novice', 'beginner', 'intermediate', 'advanced', 'expert'], { errorMap: () => ({ message: 'Invalid skill level' }) }), accessType: z.string().optional().nullable(), url: z.string().url('Must be a valid URL'), projectUrl: z.string().url('Must be a valid URL').optional().nullable(), license: z.string().optional().nullable(), knowledgebase: z.boolean().optional().nullable(), 'domain-agnostic-software': z.array(z.string()).optional().nullable(), related_concepts: z.array(z.string()).optional().nullable(), tags: z.array(z.string()).default([]), statusUrl: z.string().url('Must be a valid URL').optional().nullable() }); const ContributionRequestSchema = z.object({ action: z.enum(['add', 'edit'], { errorMap: () => ({ message: 'Action must be add or edit' }) }), tool: ContributionToolSchema, metadata: z.object({ reason: z.string().transform(val => val.trim() === '' ? undefined : val).optional() }).optional() }); // Rate limiting const rateLimitStore = new Map(); const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour const RATE_LIMIT_MAX = 15; // 15 contributions per hour per user function checkRateLimit(userId: string): boolean { const now = Date.now(); const userLimit = rateLimitStore.get(userId); if (!userLimit || now > userLimit.resetTime) { rateLimitStore.set(userId, { count: 1, resetTime: now + RATE_LIMIT_WINDOW }); return true; } if (userLimit.count >= RATE_LIMIT_MAX) { return false; } userLimit.count++; return true; } // Input sanitization function sanitizeInput(obj: any): any { if (typeof obj === 'string') { return obj.trim().slice(0, 1000); } if (Array.isArray(obj)) { return obj.map(sanitizeInput); } if (obj && typeof obj === 'object') { const sanitized: any = {}; for (const [key, value] of Object.entries(obj)) { sanitized[key] = sanitizeInput(value); } return sanitized; } return obj; } // Tool validation function async function validateToolData(tool: any, action: string): Promise<{ valid: boolean; errors: string[] }> { const errors: string[] = []; try { // Load existing data for validation const existingData = { tools: [] }; // Replace with actual data loading // Check for duplicate names (on add) if (action === 'add') { const existingNames = new Set(existingData.tools.map((t: any) => t.name.toLowerCase())); if (existingNames.has(tool.name.toLowerCase())) { errors.push('A tool with this name already exists'); } } // Type-specific validation if (tool.type === 'method') { if (tool.platforms && tool.platforms.length > 0) { errors.push('Methods should not have platform information'); } if (tool.license && tool.license !== null) { errors.push('Methods should not have license information'); } } else if (tool.type === 'software') { if (!tool.platforms || tool.platforms.length === 0) { errors.push('Software tools must specify at least one platform'); } if (!tool.license) { errors.push('Software tools must specify a license'); } } return { valid: errors.length === 0, errors }; } catch (error) { console.error('Tool validation failed:', error); errors.push('Validation failed due to system error'); return { valid: false, errors }; } } export const POST: APIRoute = async ({ request }) => { return await handleAPIRequest(async () => { // Authentication check const authResult = await withAPIAuth(request, 'contributions'); if (authResult.authRequired && !authResult.authenticated) { return apiError.unauthorized(); } const userId = authResult.session?.userId || 'anonymous'; const userEmail = authResult.session?.email || 'anon@anon.anon'; // Rate limiting if (!checkRateLimit(userId)) { return apiError.rateLimit('Rate limit exceeded. Please wait before submitting another contribution.'); } // Parse and sanitize request body let body; try { const rawBody = await request.text(); if (!rawBody.trim()) { return apiSpecial.emptyBody(); } body = JSON.parse(rawBody); } catch (error) { return apiSpecial.invalidJSON(); } // Sanitize input const sanitizedBody = sanitizeInput(body); // Validate request structure let validatedData; try { validatedData = ContributionRequestSchema.parse(sanitizedBody); } 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'); } // Additional tool-specific validation const toolValidation = await validateToolData(validatedData.tool, validatedData.action); if (!toolValidation.valid) { return apiError.validation('Tool validation failed', toolValidation.errors); } // Prepare contribution data const contributionData: ContributionData = { type: validatedData.action, tool: validatedData.tool, metadata: { submitter: userEmail, reason: validatedData.metadata?.reason } }; // CRITICAL FIX: Enhanced error handling for Git operations try { // Submit contribution via Git (now creates issue instead of PR) const gitManager = new GitContributionManager(); const result = await gitManager.submitContribution(contributionData); if (result.success) { console.log(`[CONTRIBUTION] Issue created for "${validatedData.tool.name}" by ${userEmail} - Issue: ${result.issueUrl}`); return apiResponse.created({ success: true, message: result.message, issueUrl: result.issueUrl, issueNumber: result.issueNumber }); } else { console.error(`[CONTRIBUTION FAILED] "${validatedData.tool.name}" by ${userEmail}: ${result.message}`); return apiServerError.internal(`Contribution failed: ${result.message}`); } } catch (gitError) { // CRITICAL: Handle Git operation errors properly console.error(`[GIT ERROR] ${validatedData.action} "${validatedData.tool.name}" by ${userEmail}:`, gitError); // Return proper error response const errorMessage = gitError instanceof Error ? gitError.message : 'Git operation failed'; return apiServerError.internal(`Git operation failed: ${errorMessage}`); } }, 'Contribution processing failed'); };