2025-07-26 14:07:18 +02:00

219 lines
7.6 KiB
TypeScript

// 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<string, { count: number; resetTime: number }>();
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');
};