overcuriousity d1c297189d cleanup
2025-08-12 22:34:11 +02:00

153 lines
5.4 KiB
TypeScript

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