remove dev comments
This commit is contained in:
@@ -17,12 +17,10 @@ function getEnv(key: string): string {
|
||||
|
||||
const AI_MODEL = getEnv('AI_MODEL');
|
||||
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
|
||||
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
|
||||
const RATE_LIMIT_MAX = 10; // 10 requests per minute per user
|
||||
const RATE_LIMIT_WINDOW = 60 * 1000;
|
||||
const RATE_LIMIT_MAX = 10;
|
||||
|
||||
// Input validation and sanitization
|
||||
function sanitizeInput(input: string): string {
|
||||
// Remove any content that looks like system instructions
|
||||
let sanitized = input
|
||||
.replace(/```[\s\S]*?```/g, '[CODE_BLOCK_REMOVED]') // Remove code blocks
|
||||
.replace(/\<\/?[^>]+(>|$)/g, '') // Remove HTML tags
|
||||
@@ -30,22 +28,18 @@ function sanitizeInput(input: string): string {
|
||||
.replace(/\b(ignore|forget|disregard)\s+(previous|all|your)\s+(instructions?|context|rules?)/gi, '[INSTRUCTION_REMOVED]')
|
||||
.trim();
|
||||
|
||||
// Limit length and remove excessive whitespace
|
||||
sanitized = sanitized.slice(0, 2000).replace(/\s+/g, ' ');
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
// Strip markdown code blocks from AI response
|
||||
function stripMarkdownJson(content: string): string {
|
||||
// Remove ```json and ``` wrappers
|
||||
return content
|
||||
.replace(/^```json\s*/i, '')
|
||||
.replace(/\s*```\s*$/, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
// Rate limiting check
|
||||
function checkRateLimit(userId: string): boolean {
|
||||
const now = Date.now();
|
||||
const userLimit = rateLimitStore.get(userId);
|
||||
@@ -74,7 +68,6 @@ function cleanupExpiredRateLimits() {
|
||||
|
||||
setInterval(cleanupExpiredRateLimits, 5 * 60 * 1000);
|
||||
|
||||
// Load tools database
|
||||
async function loadToolsDatabase() {
|
||||
try {
|
||||
return await getCompressedToolsDataForAI();
|
||||
@@ -84,7 +77,6 @@ async function loadToolsDatabase() {
|
||||
}
|
||||
}
|
||||
|
||||
// Create system prompt for workflow mode
|
||||
function createWorkflowSystemPrompt(toolsData: any): string {
|
||||
const toolsList = toolsData.tools.map((tool: any) => ({
|
||||
name: tool.name,
|
||||
@@ -99,7 +91,6 @@ function createWorkflowSystemPrompt(toolsData: any): string {
|
||||
related_concepts: tool.related_concepts || []
|
||||
}));
|
||||
|
||||
// Include concepts for background knowledge
|
||||
const conceptsList = toolsData.concepts.map((concept: any) => ({
|
||||
name: concept.name,
|
||||
description: concept.description,
|
||||
@@ -109,13 +100,10 @@ function createWorkflowSystemPrompt(toolsData: any): string {
|
||||
tags: concept.tags
|
||||
}));
|
||||
|
||||
// Get regular phases
|
||||
const regularPhases = toolsData.phases || [];
|
||||
|
||||
// Get domain-agnostic software phases
|
||||
const domainAgnosticSoftware = toolsData['domain-agnostic-software'] || [];
|
||||
|
||||
// Combine all phases for the description
|
||||
const allPhaseItems = [
|
||||
...regularPhases,
|
||||
...domainAgnosticSoftware
|
||||
@@ -125,22 +113,18 @@ function createWorkflowSystemPrompt(toolsData: any): string {
|
||||
`- ${phase.id}: ${phase.name}`
|
||||
).join('\n');
|
||||
|
||||
// Dynamically build domains list from configuration
|
||||
const domainsDescription = toolsData.domains.map((domain: any) =>
|
||||
`- ${domain.id}: ${domain.name}`
|
||||
).join('\n');
|
||||
|
||||
// Build dynamic phase descriptions for tool selection
|
||||
const phaseDescriptions = regularPhases.map((phase: any) =>
|
||||
`- ${phase.name}: ${phase.description || 'Tools/Methods for this phase'}`
|
||||
).join('\n');
|
||||
|
||||
// Add domain-agnostic software descriptions
|
||||
const domainAgnosticDescriptions = domainAgnosticSoftware.map((section: any) =>
|
||||
`- ${section.name}: ${section.description || 'Cross-cutting software and platforms'}`
|
||||
).join('\n');
|
||||
|
||||
// Create valid phase values for JSON schema
|
||||
const validPhases = [
|
||||
...regularPhases.map((p: any) => p.id),
|
||||
...domainAgnosticSoftware.map((s: any) => s.id)
|
||||
@@ -201,7 +185,6 @@ ANTWORT-FORMAT (strict JSON):
|
||||
Antworte NUR mit validen JSON. Keine zusätzlichen Erklärungen außerhalb des JSON.`;
|
||||
}
|
||||
|
||||
// Create system prompt for tool-specific mode
|
||||
function createToolSystemPrompt(toolsData: any): string {
|
||||
const toolsList = toolsData.tools.map((tool: any) => ({
|
||||
name: tool.name,
|
||||
@@ -217,7 +200,6 @@ function createToolSystemPrompt(toolsData: any): string {
|
||||
related_concepts: tool.related_concepts || []
|
||||
}));
|
||||
|
||||
// Include concepts for background knowledge
|
||||
const conceptsList = toolsData.concepts.map((concept: any) => ({
|
||||
name: concept.name,
|
||||
description: concept.description,
|
||||
@@ -277,7 +259,6 @@ Antworte NUR mit validen JSON. Keine zusätzlichen Erklärungen außerhalb des J
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
// Authentication check
|
||||
const authResult = await withAPIAuth(request, 'ai');
|
||||
if (!authResult.authenticated) {
|
||||
return createAuthErrorResponse();
|
||||
@@ -285,16 +266,13 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
|
||||
const userId = authResult.userId;
|
||||
|
||||
// Rate limiting
|
||||
if (!checkRateLimit(userId)) {
|
||||
return apiError.rateLimit('Rate limit exceeded');
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
const body = await request.json();
|
||||
const { query, mode = 'workflow', taskId: clientTaskId } = body;
|
||||
|
||||
// Validation
|
||||
if (!query || typeof query !== 'string') {
|
||||
return apiError.badRequest('Query required');
|
||||
}
|
||||
@@ -303,24 +281,19 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
return apiError.badRequest('Invalid mode. Must be "workflow" or "tool"');
|
||||
}
|
||||
|
||||
// Sanitize input
|
||||
const sanitizedQuery = sanitizeInput(query);
|
||||
if (sanitizedQuery.includes('[FILTERED]')) {
|
||||
return apiError.badRequest('Invalid input detected');
|
||||
}
|
||||
|
||||
// Load tools database
|
||||
const toolsData = await loadToolsDatabase();
|
||||
|
||||
// Create appropriate system prompt based on mode
|
||||
const systemPrompt = mode === 'workflow'
|
||||
? createWorkflowSystemPrompt(toolsData)
|
||||
: createToolSystemPrompt(toolsData);
|
||||
|
||||
// Generate task ID for queue tracking (use client-provided ID if available)
|
||||
const taskId = clientTaskId || `ai_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
|
||||
|
||||
// Make AI API call through rate-limited queue
|
||||
const aiResponse = await enqueueApiCall(() =>
|
||||
fetch(process.env.AI_API_ENDPOINT + '/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
@@ -346,7 +319,6 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
})
|
||||
, taskId);
|
||||
|
||||
// AI response handling
|
||||
if (!aiResponse.ok) {
|
||||
console.error('AI API error:', await aiResponse.text());
|
||||
return apiServerError.unavailable('AI service unavailable');
|
||||
@@ -359,7 +331,6 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
return apiServerError.unavailable('No response from AI');
|
||||
}
|
||||
|
||||
// Parse AI JSON response
|
||||
let recommendation;
|
||||
try {
|
||||
const cleanedContent = stripMarkdownJson(aiContent);
|
||||
@@ -369,7 +340,6 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
return apiServerError.unavailable('Invalid AI response format');
|
||||
}
|
||||
|
||||
// Validate tool names and concept names against database
|
||||
const validToolNames = new Set(toolsData.tools.map((t: any) => t.name));
|
||||
const validConceptNames = new Set(toolsData.concepts.map((c: any) => c.name));
|
||||
|
||||
@@ -404,8 +374,8 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
return true;
|
||||
}).map((tool: any, index: number) => ({
|
||||
...tool,
|
||||
rank: tool.rank || (index + 1), // Ensure rank is set
|
||||
suitability_score: tool.suitability_score || 'medium', // Default suitability
|
||||
rank: tool.rank || (index + 1),
|
||||
suitability_score: tool.suitability_score || 'medium',
|
||||
pros: Array.isArray(tool.pros) ? tool.pros : [],
|
||||
cons: Array.isArray(tool.cons) ? tool.cons : []
|
||||
})) || [],
|
||||
@@ -419,10 +389,8 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
};
|
||||
}
|
||||
|
||||
// Log successful query
|
||||
console.log(`[AI Query] Mode: ${mode}, User: ${userId}, Query length: ${sanitizedQuery.length}, Tools: ${validatedRecommendation.recommended_tools.length}, Concepts: ${validatedRecommendation.background_knowledge?.length || 0}`);
|
||||
|
||||
// Success response with task ID
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
mode,
|
||||
|
||||
@@ -8,17 +8,14 @@ export const GET: APIRoute = async ({ url, redirect }) => {
|
||||
const state = generateState();
|
||||
const authUrl = generateAuthUrl(state);
|
||||
|
||||
// Debug: log the generated URL
|
||||
console.log('Generated auth URL:', authUrl);
|
||||
|
||||
// Get the intended destination after login (if any)
|
||||
const returnTo = url.searchParams.get('returnTo') || '/';
|
||||
|
||||
logAuthEvent('Login initiated', { returnTo, authUrl });
|
||||
|
||||
// Store state and returnTo in a cookie for the callback
|
||||
const stateData = JSON.stringify({ state, returnTo });
|
||||
const stateCookie = `auth_state=${encodeURIComponent(stateData)}; HttpOnly; SameSite=Lax; Path=/; Max-Age=600`; // 10 minutes
|
||||
const stateCookie = `auth_state=${encodeURIComponent(stateData)}; HttpOnly; SameSite=Lax; Path=/; Max-Age=600`;
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
|
||||
@@ -13,7 +13,6 @@ export const prerender = false;
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
return await handleAPIRequest(async () => {
|
||||
// Parse request body
|
||||
let body;
|
||||
try {
|
||||
body = await request.json();
|
||||
@@ -29,17 +28,14 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
return apiSpecial.missingRequired(['code', 'state']);
|
||||
}
|
||||
|
||||
// CONSOLIDATED: Single function call replaces 15+ lines of duplicated state verification
|
||||
const stateVerification = verifyAuthState(request, state);
|
||||
if (!stateVerification.isValid || !stateVerification.stateData) {
|
||||
return apiError.badRequest(stateVerification.error || 'Invalid state parameter');
|
||||
}
|
||||
|
||||
// Exchange code for tokens and get user info
|
||||
const tokens = await exchangeCodeForTokens(code);
|
||||
const userInfo = await getUserInfo(tokens.access_token);
|
||||
|
||||
// CONSOLIDATED: Single function call replaces 10+ lines of session creation
|
||||
const sessionResult = await createSessionWithCookie(userInfo);
|
||||
|
||||
logAuthEvent('Authentication successful', {
|
||||
@@ -47,11 +43,9 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
email: sessionResult.userEmail
|
||||
});
|
||||
|
||||
// FIXED: Create response with multiple Set-Cookie headers
|
||||
const responseHeaders = new Headers();
|
||||
responseHeaders.set('Content-Type', 'application/json');
|
||||
|
||||
// Each cookie needs its own Set-Cookie header
|
||||
responseHeaders.append('Set-Cookie', sessionResult.sessionCookie);
|
||||
responseHeaders.append('Set-Cookie', sessionResult.clearStateCookie);
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { z } from 'zod';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
// Simple schema - all fields optional except for having some content
|
||||
const KnowledgebaseContributionSchema = z.object({
|
||||
toolName: z.string().optional().nullable().transform(val => val || undefined),
|
||||
title: z.string().optional().nullable().transform(val => val || undefined),
|
||||
@@ -40,7 +39,6 @@ interface KnowledgebaseContributionData {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
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
|
||||
@@ -63,8 +61,6 @@ function checkRateLimit(userEmail: string): boolean {
|
||||
}
|
||||
|
||||
function validateKnowledgebaseData(data: KnowledgebaseContributionData): { valid: boolean; errors?: string[] } {
|
||||
// Very minimal validation - just check that SOMETHING was provided
|
||||
// Use nullish coalescing to avoid “possibly undefined” errors in strict mode
|
||||
const hasContent = (data.content ?? '').trim().length > 0;
|
||||
const hasLink = (data.externalLink ?? '').trim().length > 0;
|
||||
const hasFiles = Array.isArray(data.uploadedFiles) && data.uploadedFiles.length > 0;
|
||||
@@ -83,7 +79,6 @@ function validateKnowledgebaseData(data: KnowledgebaseContributionData): { valid
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
return await handleAPIRequest(async () => {
|
||||
// Check authentication
|
||||
const authResult = await withAPIAuth(request, 'contributions');
|
||||
if (authResult.authRequired && !authResult.authenticated) {
|
||||
return apiError.unauthorized();
|
||||
@@ -91,12 +86,10 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
|
||||
const userEmail = authResult.session?.email || 'anon@anon.anon';
|
||||
|
||||
// Rate limiting
|
||||
if (!checkRateLimit(userEmail)) {
|
||||
return apiError.rateLimit('Rate limit exceeded. Please wait before submitting again.');
|
||||
}
|
||||
|
||||
// Parse form data
|
||||
let formData;
|
||||
try {
|
||||
formData = await request.formData();
|
||||
@@ -106,7 +99,6 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
|
||||
const rawData = Object.fromEntries(formData);
|
||||
|
||||
// Validate and sanitize data
|
||||
let validatedData;
|
||||
try {
|
||||
validatedData = KnowledgebaseContributionSchema.parse(rawData);
|
||||
@@ -121,13 +113,11 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
return apiError.badRequest('Invalid request data');
|
||||
}
|
||||
|
||||
// Basic content validation
|
||||
const contentValidation = validateKnowledgebaseData(validatedData);
|
||||
if (!contentValidation.valid) {
|
||||
return apiError.validation('Content validation failed', contentValidation.errors);
|
||||
}
|
||||
|
||||
// Submit as issue via Git
|
||||
try {
|
||||
const gitManager = new GitContributionManager();
|
||||
const result = await gitManager.submitKnowledgebaseContribution({
|
||||
|
||||
@@ -7,7 +7,6 @@ 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(),
|
||||
@@ -42,7 +41,6 @@ const ContributionRequestSchema = z.object({
|
||||
}).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
|
||||
@@ -64,7 +62,6 @@ function checkRateLimit(userId: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Input sanitization
|
||||
function sanitizeInput(obj: any): any {
|
||||
if (typeof obj === 'string') {
|
||||
return obj.trim().slice(0, 1000);
|
||||
@@ -82,15 +79,12 @@ function sanitizeInput(obj: any): any {
|
||||
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
|
||||
const existingData = { tools: [] };
|
||||
|
||||
// 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())) {
|
||||
@@ -98,7 +92,6 @@ async function validateToolData(tool: any, action: string): Promise<{ valid: boo
|
||||
}
|
||||
}
|
||||
|
||||
// Type-specific validation
|
||||
if (tool.type === 'method') {
|
||||
if (tool.platforms && tool.platforms.length > 0) {
|
||||
errors.push('Methods should not have platform information');
|
||||
@@ -126,7 +119,6 @@ async function validateToolData(tool: any, action: string): Promise<{ valid: boo
|
||||
|
||||
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();
|
||||
@@ -135,12 +127,10 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
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();
|
||||
@@ -152,10 +142,8 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
return apiSpecial.invalidJSON();
|
||||
}
|
||||
|
||||
// Sanitize input
|
||||
const sanitizedBody = sanitizeInput(body);
|
||||
|
||||
// Validate request structure
|
||||
let validatedData;
|
||||
try {
|
||||
validatedData = ContributionRequestSchema.parse(sanitizedBody);
|
||||
@@ -170,13 +158,11 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
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,
|
||||
@@ -186,9 +172,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -207,10 +191,8 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
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}`);
|
||||
}
|
||||
|
||||
@@ -18,15 +18,11 @@ interface UploadResult {
|
||||
storage?: 'nextcloud' | 'local';
|
||||
}
|
||||
|
||||
// Configuration
|
||||
const UPLOAD_CONFIG = {
|
||||
maxFileSize: 50 * 1024 * 1024, // 50MB
|
||||
allowedTypes: new Set([
|
||||
// Images
|
||||
'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
|
||||
// Videos
|
||||
'video/mp4', 'video/webm', 'video/ogg', 'video/avi', 'video/mov',
|
||||
// Documents
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
@@ -40,10 +36,9 @@ const UPLOAD_CONFIG = {
|
||||
publicBaseUrl: process.env.PUBLIC_BASE_URL || 'http://localhost:4321'
|
||||
};
|
||||
|
||||
// Rate limiting for uploads
|
||||
const uploadRateLimit = new Map<string, { count: number; resetTime: number }>();
|
||||
const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour
|
||||
const RATE_LIMIT_MAX = 100; // Max 100 uploads per hour per user
|
||||
const RATE_LIMIT_MAX = 10; // Max 10 uploads per hour per user
|
||||
|
||||
function checkUploadRateLimit(userEmail: string): boolean {
|
||||
const now = Date.now();
|
||||
@@ -63,7 +58,6 @@ function checkUploadRateLimit(userEmail: string): boolean {
|
||||
}
|
||||
|
||||
function validateFile(file: File): { valid: boolean; error?: string } {
|
||||
// File size check
|
||||
if (file.size > UPLOAD_CONFIG.maxFileSize) {
|
||||
return {
|
||||
valid: false,
|
||||
@@ -71,7 +65,6 @@ function validateFile(file: File): { valid: boolean; error?: string } {
|
||||
};
|
||||
}
|
||||
|
||||
// File type check
|
||||
if (!UPLOAD_CONFIG.allowedTypes.has(file.type)) {
|
||||
return {
|
||||
valid: false,
|
||||
@@ -105,21 +98,17 @@ async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadR
|
||||
|
||||
async function uploadToLocal(file: File, userType: string): Promise<UploadResult> {
|
||||
try {
|
||||
// Ensure upload directory exists
|
||||
await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
|
||||
|
||||
// Generate unique filename
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const randomString = crypto.randomBytes(8).toString('hex');
|
||||
const extension = path.extname(file.name);
|
||||
const filename = `${timestamp}-${randomString}${extension}`;
|
||||
|
||||
// Save file
|
||||
const filepath = path.join(UPLOAD_CONFIG.localUploadPath, filename);
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
await fs.writeFile(filepath, buffer);
|
||||
|
||||
// Generate public URL
|
||||
const publicUrl = `${UPLOAD_CONFIG.publicBaseUrl}/uploads/${filename}`;
|
||||
|
||||
return {
|
||||
@@ -166,19 +155,16 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
return apiSpecial.missingRequired(['file']);
|
||||
}
|
||||
|
||||
// Validate file
|
||||
const validation = validateFile(file);
|
||||
if (!validation.valid) {
|
||||
return apiError.badRequest(validation.error!);
|
||||
}
|
||||
|
||||
// Attempt upload (Nextcloud first, then local fallback)
|
||||
let result: UploadResult;
|
||||
|
||||
if (isNextcloudConfigured()) {
|
||||
result = await uploadToNextcloud(file, userEmail);
|
||||
|
||||
// If Nextcloud fails, try local fallback
|
||||
if (!result.success) {
|
||||
console.warn('Nextcloud upload failed, trying local fallback:', result.error);
|
||||
result = await uploadToLocal(file, type);
|
||||
@@ -188,16 +174,8 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
// Log successful upload
|
||||
console.log(`[MEDIA UPLOAD] ${file.name} (${file.size} bytes) by ${userEmail} -> ${result.storage}: ${result.url}`);
|
||||
|
||||
// BEFORE: Manual success response (5 lines)
|
||||
// return new Response(JSON.stringify(result), {
|
||||
// status: 200,
|
||||
// headers: { 'Content-Type': 'application/json' }
|
||||
// });
|
||||
|
||||
// AFTER: Single line with specialized helper
|
||||
return apiSpecial.uploadSuccess({
|
||||
url: result.url!,
|
||||
filename: result.filename!,
|
||||
@@ -205,35 +183,23 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
storage: result.storage!
|
||||
});
|
||||
} else {
|
||||
// Log failed upload
|
||||
console.error(`[MEDIA UPLOAD FAILED] ${file.name} by ${userEmail}: ${result.error}`);
|
||||
|
||||
// BEFORE: Manual error response (5 lines)
|
||||
// return new Response(JSON.stringify(result), {
|
||||
// status: 500,
|
||||
// headers: { 'Content-Type': 'application/json' }
|
||||
// });
|
||||
|
||||
// AFTER: Single line with specialized helper
|
||||
return apiSpecial.uploadFailed(result.error!);
|
||||
}
|
||||
|
||||
}, 'Media upload processing failed');
|
||||
};
|
||||
|
||||
// GET endpoint for upload status/info
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
return await handleAPIRequest(async () => {
|
||||
// Authentication check
|
||||
const authResult = await withAPIAuth(request);
|
||||
if (authResult.authRequired && !authResult.authenticated) {
|
||||
return apiError.unauthorized();
|
||||
}
|
||||
|
||||
// Return upload configuration and status
|
||||
const nextcloudConfigured = isNextcloudConfigured();
|
||||
|
||||
// Check local upload directory
|
||||
let localStorageAvailable = false;
|
||||
try {
|
||||
await fs.access(UPLOAD_CONFIG.localUploadPath);
|
||||
|
||||
Reference in New Issue
Block a user