151 lines
5.4 KiB
TypeScript
151 lines
5.4 KiB
TypeScript
// src/pages/api/contribute/knowledgebase.ts - SIMPLIFIED: Issues only, minimal validation
|
|
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)
|
|
});
|
|
|
|
interface KnowledgebaseContributionData {
|
|
toolName?: string;
|
|
title?: string;
|
|
description?: string;
|
|
content?: string;
|
|
externalLink?: string;
|
|
difficulty?: string;
|
|
categories: string[];
|
|
tags: string[];
|
|
uploadedFiles: any[];
|
|
reason?: string;
|
|
}
|
|
|
|
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
|
|
const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour
|
|
const RATE_LIMIT_MAX = 3; // Max 3 submissions per hour per user
|
|
|
|
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');
|
|
};
|