Merge pull request 'upload-issue' (#9) from upload-issue into main
Reviewed-on: #9
This commit is contained in:
		
						commit
						6e9f37c1cd
					
				
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -142,6 +142,7 @@ WantedBy=multi-user.target
 | 
				
			|||||||
server {
 | 
					server {
 | 
				
			||||||
    listen 80;
 | 
					    listen 80;
 | 
				
			||||||
    server_name forensic-pathways.yourdomain.com;
 | 
					    server_name forensic-pathways.yourdomain.com;
 | 
				
			||||||
 | 
					    client_max_body_size 50M; # Important for uploads
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    location / {
 | 
					    location / {
 | 
				
			||||||
        proxy_pass http://localhost:4321;
 | 
					        proxy_pass http://localhost:4321;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
// src/pages/api/upload/media.ts 
 | 
					// src/pages/api/upload/media.ts - Enhanced with detailed logging and error handling
 | 
				
			||||||
import type { APIRoute } from 'astro';
 | 
					import type { APIRoute } from 'astro';
 | 
				
			||||||
import { withAPIAuth } from '../../../utils/auth.js';
 | 
					import { withAPIAuth } from '../../../utils/auth.js';
 | 
				
			||||||
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
 | 
					import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
 | 
				
			||||||
@ -21,8 +21,13 @@ interface UploadResult {
 | 
				
			|||||||
const UPLOAD_CONFIG = {
 | 
					const UPLOAD_CONFIG = {
 | 
				
			||||||
  maxFileSize: 50 * 1024 * 1024, // 50MB
 | 
					  maxFileSize: 50 * 1024 * 1024, // 50MB
 | 
				
			||||||
  allowedTypes: new Set([
 | 
					  allowedTypes: new Set([
 | 
				
			||||||
 | 
					    // Images
 | 
				
			||||||
    'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
 | 
					    'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Videos
 | 
				
			||||||
    'video/mp4', 'video/webm', 'video/ogg', 'video/avi', 'video/mov',
 | 
					    'video/mp4', 'video/webm', 'video/ogg', 'video/avi', 'video/mov',
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Documents
 | 
				
			||||||
    'application/pdf',
 | 
					    'application/pdf',
 | 
				
			||||||
    'application/msword',
 | 
					    'application/msword',
 | 
				
			||||||
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
 | 
					    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
 | 
				
			||||||
@ -30,7 +35,32 @@ const UPLOAD_CONFIG = {
 | 
				
			|||||||
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
 | 
					    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
 | 
				
			||||||
    'application/vnd.ms-powerpoint',
 | 
					    'application/vnd.ms-powerpoint',
 | 
				
			||||||
    'application/vnd.openxmlformats-officedocument.presentationml.presentation',
 | 
					    'application/vnd.openxmlformats-officedocument.presentationml.presentation',
 | 
				
			||||||
    'text/plain', 'text/csv', 'application/json'
 | 
					    
 | 
				
			||||||
 | 
					    // Text files
 | 
				
			||||||
 | 
					    'text/plain', 
 | 
				
			||||||
 | 
					    'text/csv', 
 | 
				
			||||||
 | 
					    'text/markdown',           // Added markdown
 | 
				
			||||||
 | 
					    'text/x-markdown',         // Alternative markdown MIME type
 | 
				
			||||||
 | 
					    'application/json',
 | 
				
			||||||
 | 
					    'application/xml',
 | 
				
			||||||
 | 
					    'text/xml',
 | 
				
			||||||
 | 
					    'text/html',
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Archives
 | 
				
			||||||
 | 
					    'application/zip', 
 | 
				
			||||||
 | 
					    'application/x-tar', 
 | 
				
			||||||
 | 
					    'application/gzip',
 | 
				
			||||||
 | 
					    'application/x-gzip',
 | 
				
			||||||
 | 
					    'application/x-zip-compressed',
 | 
				
			||||||
 | 
					    'application/x-rar-compressed',
 | 
				
			||||||
 | 
					    'application/x-7z-compressed',
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Additional useful formats
 | 
				
			||||||
 | 
					    'application/rtf',         // Rich Text Format
 | 
				
			||||||
 | 
					    'text/richtext',
 | 
				
			||||||
 | 
					    'application/x-yaml',      // YAML files
 | 
				
			||||||
 | 
					    'text/yaml',
 | 
				
			||||||
 | 
					    'application/yaml'
 | 
				
			||||||
  ]),
 | 
					  ]),
 | 
				
			||||||
  localUploadPath: process.env.LOCAL_UPLOAD_PATH || './public/uploads',
 | 
					  localUploadPath: process.env.LOCAL_UPLOAD_PATH || './public/uploads',
 | 
				
			||||||
  publicBaseUrl: process.env.PUBLIC_BASE_URL || 'http://localhost:4321'
 | 
					  publicBaseUrl: process.env.PUBLIC_BASE_URL || 'http://localhost:4321'
 | 
				
			||||||
@ -50,6 +80,7 @@ function checkUploadRateLimit(userEmail: string): boolean {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  if (userLimit.count >= RATE_LIMIT_MAX) {
 | 
					  if (userLimit.count >= RATE_LIMIT_MAX) {
 | 
				
			||||||
 | 
					    console.warn(`[UPLOAD] Rate limit exceeded for user: ${userEmail} (${userLimit.count}/${RATE_LIMIT_MAX})`);
 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
@ -58,27 +89,37 @@ function checkUploadRateLimit(userEmail: string): boolean {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function validateFile(file: File): { valid: boolean; error?: string } {
 | 
					function validateFile(file: File): { valid: boolean; error?: string } {
 | 
				
			||||||
 | 
					  console.log(`[UPLOAD] Validating file: ${file.name}, size: ${file.size}, type: ${file.type}`);
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
  if (file.size > UPLOAD_CONFIG.maxFileSize) {
 | 
					  if (file.size > UPLOAD_CONFIG.maxFileSize) {
 | 
				
			||||||
    return { 
 | 
					    const errorMsg = `File too large. Maximum size is ${Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024)}MB`;
 | 
				
			||||||
      valid: false, 
 | 
					    console.warn(`[UPLOAD] ${errorMsg} - File size: ${file.size}`);
 | 
				
			||||||
      error: `File too large. Maximum size is ${Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024)}MB` 
 | 
					    return { valid: false, error: errorMsg };
 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  if (!UPLOAD_CONFIG.allowedTypes.has(file.type)) {
 | 
					  if (!UPLOAD_CONFIG.allowedTypes.has(file.type)) {
 | 
				
			||||||
    return { 
 | 
					    const errorMsg = `File type ${file.type} not allowed`;
 | 
				
			||||||
      valid: false, 
 | 
					    console.warn(`[UPLOAD] ${errorMsg} - Allowed types:`, Array.from(UPLOAD_CONFIG.allowedTypes));
 | 
				
			||||||
      error: `File type ${file.type} not allowed` 
 | 
					    return { valid: false, error: errorMsg };
 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
 | 
					  console.log(`[UPLOAD] File validation passed for: ${file.name}`);
 | 
				
			||||||
  return { valid: true };
 | 
					  return { valid: true };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadResult> {
 | 
					async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadResult> {
 | 
				
			||||||
 | 
					  console.log(`[UPLOAD] Attempting Nextcloud upload for: ${file.name} by ${userEmail}`);
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const uploader = new NextcloudUploader();
 | 
					    const uploader = new NextcloudUploader();
 | 
				
			||||||
    const result = await uploader.uploadFile(file, userEmail);
 | 
					    const result = await uploader.uploadFile(file, userEmail);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    console.log(`[UPLOAD] Nextcloud upload successful:`, {
 | 
				
			||||||
 | 
					      filename: result.filename,
 | 
				
			||||||
 | 
					      url: result.url,
 | 
				
			||||||
 | 
					      size: file.size
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      success: true,
 | 
					      success: true,
 | 
				
			||||||
      url: result.url,
 | 
					      url: result.url,
 | 
				
			||||||
@ -87,7 +128,7 @@ async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadR
 | 
				
			|||||||
      storage: 'nextcloud'
 | 
					      storage: 'nextcloud'
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
    console.error('Nextcloud upload failed:', error);
 | 
					    console.error('[UPLOAD] Nextcloud upload failed:', error);
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      success: false,
 | 
					      success: false,
 | 
				
			||||||
      error: error instanceof Error ? error.message : 'Nextcloud upload failed',
 | 
					      error: error instanceof Error ? error.message : 'Nextcloud upload failed',
 | 
				
			||||||
@ -97,7 +138,10 @@ async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadR
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function uploadToLocal(file: File, userType: string): Promise<UploadResult> {
 | 
					async function uploadToLocal(file: File, userType: string): Promise<UploadResult> {
 | 
				
			||||||
 | 
					  console.log(`[UPLOAD] Attempting local upload for: ${file.name} (${userType})`);
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
 | 
					    console.log(`[UPLOAD] Creating directory: ${UPLOAD_CONFIG.localUploadPath}`);
 | 
				
			||||||
    await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
 | 
					    await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
 | 
					    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
 | 
				
			||||||
@ -106,11 +150,20 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
 | 
				
			|||||||
    const filename = `${timestamp}-${randomString}${extension}`;
 | 
					    const filename = `${timestamp}-${randomString}${extension}`;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    const filepath = path.join(UPLOAD_CONFIG.localUploadPath, filename);
 | 
					    const filepath = path.join(UPLOAD_CONFIG.localUploadPath, filename);
 | 
				
			||||||
 | 
					    console.log(`[UPLOAD] Writing file to: ${filepath}`);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    const buffer = Buffer.from(await file.arrayBuffer());
 | 
					    const buffer = Buffer.from(await file.arrayBuffer());
 | 
				
			||||||
    await fs.writeFile(filepath, buffer);
 | 
					    await fs.writeFile(filepath, buffer);
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    const publicUrl = `${UPLOAD_CONFIG.publicBaseUrl}/uploads/${filename}`;
 | 
					    const publicUrl = `${UPLOAD_CONFIG.publicBaseUrl}/uploads/${filename}`;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    console.log(`[UPLOAD] Local upload successful:`, {
 | 
				
			||||||
 | 
					      filename,
 | 
				
			||||||
 | 
					      filepath,
 | 
				
			||||||
 | 
					      publicUrl,
 | 
				
			||||||
 | 
					      size: file.size
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      success: true,
 | 
					      success: true,
 | 
				
			||||||
      url: publicUrl,
 | 
					      url: publicUrl,
 | 
				
			||||||
@ -119,7 +172,7 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
 | 
				
			|||||||
      storage: 'local'
 | 
					      storage: 'local'
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
    console.error('Local upload failed:', error);
 | 
					    console.error('[UPLOAD] Local upload failed:', error);
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      success: false,
 | 
					      success: false,
 | 
				
			||||||
      error: error instanceof Error ? error.message : 'Local upload failed',
 | 
					      error: error instanceof Error ? error.message : 'Local upload failed',
 | 
				
			||||||
@ -130,12 +183,23 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const POST: APIRoute = async ({ request }) => {
 | 
					export const POST: APIRoute = async ({ request }) => {
 | 
				
			||||||
  return await handleAPIRequest(async () => {
 | 
					  return await handleAPIRequest(async () => {
 | 
				
			||||||
 | 
					    console.log('[UPLOAD] Processing upload request');
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Enhanced auth logging
 | 
				
			||||||
    const authResult = await withAPIAuth(request, 'contributions');
 | 
					    const authResult = await withAPIAuth(request, 'contributions');
 | 
				
			||||||
 | 
					    console.log('[UPLOAD] Auth result:', {
 | 
				
			||||||
 | 
					      authenticated: authResult.authenticated,
 | 
				
			||||||
 | 
					      authRequired: authResult.authRequired,
 | 
				
			||||||
 | 
					      userId: authResult.userId
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    if (authResult.authRequired && !authResult.authenticated) {
 | 
					    if (authResult.authRequired && !authResult.authenticated) {
 | 
				
			||||||
      return apiError.unauthorized();
 | 
					      console.warn('[UPLOAD] Upload rejected - authentication required but user not authenticated');
 | 
				
			||||||
 | 
					      return apiError.unauthorized('Authentication required for file uploads');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const userEmail = authResult.session?.email || 'anon@anon.anon';
 | 
					    const userEmail = authResult.session?.email || 'anon@anon.anon';
 | 
				
			||||||
 | 
					    console.log(`[UPLOAD] Processing upload for user: ${userEmail}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!checkUploadRateLimit(userEmail)) {
 | 
					    if (!checkUploadRateLimit(userEmail)) {
 | 
				
			||||||
      return apiError.rateLimit('Upload rate limit exceeded. Please wait before uploading again.');
 | 
					      return apiError.rateLimit('Upload rate limit exceeded. Please wait before uploading again.');
 | 
				
			||||||
@ -143,38 +207,60 @@ export const POST: APIRoute = async ({ request }) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    let formData;
 | 
					    let formData;
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
 | 
					      console.log('[UPLOAD] Parsing form data');
 | 
				
			||||||
      formData = await request.formData();
 | 
					      formData = await request.formData();
 | 
				
			||||||
 | 
					      console.log('[UPLOAD] Form data keys:', Array.from(formData.keys()));
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      return apiError.badRequest('Invalid form data');
 | 
					      console.error('[UPLOAD] Failed to parse form data:', error);
 | 
				
			||||||
 | 
					      return apiError.badRequest('Invalid form data - could not parse request');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const file = formData.get('file') as File;
 | 
					    const file = formData.get('file') as File;
 | 
				
			||||||
    const type = formData.get('type') as string;
 | 
					    const type = formData.get('type') as string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!file) {
 | 
					    if (!file) {
 | 
				
			||||||
 | 
					      console.warn('[UPLOAD] No file provided in request');
 | 
				
			||||||
      return apiSpecial.missingRequired(['file']);
 | 
					      return apiSpecial.missingRequired(['file']);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log(`[UPLOAD] Processing file: ${file.name}, type parameter: ${type}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const validation = validateFile(file);
 | 
					    const validation = validateFile(file);
 | 
				
			||||||
    if (!validation.valid) {
 | 
					    if (!validation.valid) {
 | 
				
			||||||
      return apiError.badRequest(validation.error!);
 | 
					      return apiError.badRequest(validation.error!);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Enhanced environment logging
 | 
				
			||||||
 | 
					    const nextcloudConfigured = isNextcloudConfigured();
 | 
				
			||||||
 | 
					    console.log('[UPLOAD] Environment check:', {
 | 
				
			||||||
 | 
					      nextcloudConfigured,
 | 
				
			||||||
 | 
					      localUploadPath: UPLOAD_CONFIG.localUploadPath,
 | 
				
			||||||
 | 
					      publicBaseUrl: UPLOAD_CONFIG.publicBaseUrl,
 | 
				
			||||||
 | 
					      nodeEnv: process.env.NODE_ENV
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let result: UploadResult;
 | 
					    let result: UploadResult;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    if (isNextcloudConfigured()) {
 | 
					    if (nextcloudConfigured) {
 | 
				
			||||||
 | 
					      console.log('[UPLOAD] Using Nextcloud as primary storage');
 | 
				
			||||||
      result = await uploadToNextcloud(file, userEmail);
 | 
					      result = await uploadToNextcloud(file, userEmail);
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      if (!result.success) {
 | 
					      if (!result.success) {
 | 
				
			||||||
        console.warn('Nextcloud upload failed, trying local fallback:', result.error);
 | 
					        console.warn('[UPLOAD] Nextcloud upload failed, trying local fallback:', result.error);
 | 
				
			||||||
        result = await uploadToLocal(file, type);
 | 
					        result = await uploadToLocal(file, type);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
 | 
					      console.log('[UPLOAD] Using local storage (Nextcloud not configured)');
 | 
				
			||||||
      result = await uploadToLocal(file, type);
 | 
					      result = await uploadToLocal(file, type);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    if (result.success) {
 | 
					    if (result.success) {
 | 
				
			||||||
      console.log(`[MEDIA UPLOAD] ${file.name} (${file.size} bytes) by ${userEmail} -> ${result.storage}: ${result.url}`);
 | 
					      console.log(`[UPLOAD] Upload completed successfully:`, {
 | 
				
			||||||
 | 
					        filename: result.filename,
 | 
				
			||||||
 | 
					        storage: result.storage,
 | 
				
			||||||
 | 
					        url: result.url,
 | 
				
			||||||
 | 
					        user: userEmail
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      return apiSpecial.uploadSuccess({
 | 
					      return apiSpecial.uploadSuccess({
 | 
				
			||||||
        url: result.url!,
 | 
					        url: result.url!,
 | 
				
			||||||
@ -183,7 +269,12 @@ export const POST: APIRoute = async ({ request }) => {
 | 
				
			|||||||
        storage: result.storage!
 | 
					        storage: result.storage!
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      console.error(`[MEDIA UPLOAD FAILED] ${file.name} by ${userEmail}: ${result.error}`);
 | 
					      console.error(`[UPLOAD] Upload failed completely:`, {
 | 
				
			||||||
 | 
					        filename: file.name,
 | 
				
			||||||
 | 
					        error: result.error,
 | 
				
			||||||
 | 
					        storage: result.storage,
 | 
				
			||||||
 | 
					        user: userEmail
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      return apiSpecial.uploadFailed(result.error!);
 | 
					      return apiSpecial.uploadFailed(result.error!);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -193,6 +284,8 @@ export const POST: APIRoute = async ({ request }) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const GET: APIRoute = async ({ request }) => {
 | 
					export const GET: APIRoute = async ({ request }) => {
 | 
				
			||||||
  return await handleAPIRequest(async () => {
 | 
					  return await handleAPIRequest(async () => {
 | 
				
			||||||
 | 
					    console.log('[UPLOAD] Getting upload status');
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    const authResult = await withAPIAuth(request);
 | 
					    const authResult = await withAPIAuth(request);
 | 
				
			||||||
    if (authResult.authRequired && !authResult.authenticated) {
 | 
					    if (authResult.authRequired && !authResult.authenticated) {
 | 
				
			||||||
      return apiError.unauthorized();
 | 
					      return apiError.unauthorized();
 | 
				
			||||||
@ -204,12 +297,14 @@ export const GET: APIRoute = async ({ request }) => {
 | 
				
			|||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await fs.access(UPLOAD_CONFIG.localUploadPath);
 | 
					      await fs.access(UPLOAD_CONFIG.localUploadPath);
 | 
				
			||||||
      localStorageAvailable = true;
 | 
					      localStorageAvailable = true;
 | 
				
			||||||
 | 
					      console.log('[UPLOAD] Local storage accessible');
 | 
				
			||||||
    } catch {
 | 
					    } catch {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
 | 
					        await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
 | 
				
			||||||
        localStorageAvailable = true;
 | 
					        localStorageAvailable = true;
 | 
				
			||||||
 | 
					        console.log('[UPLOAD] Local storage created');
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        console.warn('Local upload directory not accessible:', error);
 | 
					        console.warn('[UPLOAD] Local upload directory not accessible:', error);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
@ -237,9 +332,14 @@ export const GET: APIRoute = async ({ request }) => {
 | 
				
			|||||||
      paths: {
 | 
					      paths: {
 | 
				
			||||||
        uploadEndpoint: '/api/upload/media',
 | 
					        uploadEndpoint: '/api/upload/media',
 | 
				
			||||||
        localPath: localStorageAvailable ? '/uploads' : null
 | 
					        localPath: localStorageAvailable ? '/uploads' : null
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      environment: {
 | 
				
			||||||
 | 
					        nodeEnv: process.env.NODE_ENV,
 | 
				
			||||||
 | 
					        publicBaseUrl: UPLOAD_CONFIG.publicBaseUrl
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    console.log('[UPLOAD] Status check completed:', status);
 | 
				
			||||||
    return apiResponse.success(status);
 | 
					    return apiResponse.success(status);
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
  }, 'Upload status retrieval failed');
 | 
					  }, 'Upload status retrieval failed');
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
---
 | 
					---
 | 
				
			||||||
// src/pages/contribute/knowledgebase.astro - SIMPLIFIED: Issues only, minimal validation
 | 
					// src/pages/contribute/knowledgebase.astro
 | 
				
			||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
 | 
					import BaseLayout from '../../layouts/BaseLayout.astro';
 | 
				
			||||||
import { withAuth } from '../../utils/auth.js';
 | 
					import { withAuth } from '../../utils/auth.js';
 | 
				
			||||||
import { getToolsData } from '../../utils/dataService.js';
 | 
					import { getToolsData } from '../../utils/dataService.js';
 | 
				
			||||||
@ -114,8 +114,13 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
 | 
				
			|||||||
          <div class="form-group">
 | 
					          <div class="form-group">
 | 
				
			||||||
            <label class="form-label">Dokumente, Bilder, Videos (Optional)</label>
 | 
					            <label class="form-label">Dokumente, Bilder, Videos (Optional)</label>
 | 
				
			||||||
            <div class="upload-area" id="upload-area">
 | 
					            <div class="upload-area" id="upload-area">
 | 
				
			||||||
              <input type="file" id="file-input" multiple accept=".pdf,.doc,.docx,.txt,.md,.zip,.png,.jpg,.jpeg,.gif,.mp4,.webm" class="hidden">
 | 
					              <input 
 | 
				
			||||||
              <div class="upload-placeholder">
 | 
					                type="file" 
 | 
				
			||||||
 | 
					                id="file-input" 
 | 
				
			||||||
 | 
					                multiple 
 | 
				
			||||||
 | 
					                accept=".pdf,.doc,.docx,.txt,.md,.markdown,.csv,.json,.xml,.html,.rtf,.yaml,.yml,.zip,.tar,.gz,.rar,.7z,.png,.jpg,.jpeg,.gif,.webp,.svg,.mp4,.webm,.mov,.avi" 
 | 
				
			||||||
 | 
					                class="hidden"
 | 
				
			||||||
 | 
					              >              <div class="upload-placeholder">
 | 
				
			||||||
                <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
 | 
					                <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
 | 
				
			||||||
                  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
 | 
					                  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
 | 
				
			||||||
                  <polyline points="7 10 12 15 17 10"/>
 | 
					                  <polyline points="7 10 12 15 17 10"/>
 | 
				
			||||||
@ -304,6 +309,13 @@ class KnowledgebaseForm {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private handleFiles(files: File[]) {
 | 
					  private handleFiles(files: File[]) {
 | 
				
			||||||
    files.forEach(file => {
 | 
					    files.forEach(file => {
 | 
				
			||||||
 | 
					      // Client-side validation before upload
 | 
				
			||||||
 | 
					      const validation = this.validateFileBeforeUpload(file);
 | 
				
			||||||
 | 
					      if (!validation.valid) {
 | 
				
			||||||
 | 
					        console.log('[UPLOAD]Cannot upload ', file.name, ' Error: ', validation.error);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const fileId = Date.now() + '-' + Math.random().toString(36).substr(2, 9);
 | 
					      const fileId = Date.now() + '-' + Math.random().toString(36).substr(2, 9);
 | 
				
			||||||
      const newFile: UploadedFile = {
 | 
					      const newFile: UploadedFile = {
 | 
				
			||||||
        id: fileId,
 | 
					        id: fileId,
 | 
				
			||||||
@ -317,30 +329,99 @@ class KnowledgebaseForm {
 | 
				
			|||||||
    this.renderFileList();
 | 
					    this.renderFileList();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private validateFileBeforeUpload(file: File): { valid: boolean; error?: string } {
 | 
				
			||||||
 | 
					    const maxSizeBytes = 50 * 1024 * 1024; // 50MB
 | 
				
			||||||
 | 
					    if (file.size > maxSizeBytes) {
 | 
				
			||||||
 | 
					      const sizeMB = (file.size / 1024 / 1024).toFixed(1);
 | 
				
			||||||
 | 
					      const maxMB = (maxSizeBytes / 1024 / 1024).toFixed(0);
 | 
				
			||||||
 | 
					      return { 
 | 
				
			||||||
 | 
					        valid: false, 
 | 
				
			||||||
 | 
					        error: `File too large (${sizeMB}MB). Maximum size: ${maxMB}MB` 
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check file type
 | 
				
			||||||
 | 
					    const allowedExtensions = [
 | 
				
			||||||
 | 
					      '.pdf', '.doc', '.docx', '.txt', '.md', '.markdown', '.csv', '.json', 
 | 
				
			||||||
 | 
					      '.xml', '.html', '.rtf', '.yaml', '.yml', '.zip', '.tar', '.gz', 
 | 
				
			||||||
 | 
					      '.rar', '.7z', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', 
 | 
				
			||||||
 | 
					      '.mp4', '.webm', '.mov', '.avi'
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    const fileName = file.name.toLowerCase();
 | 
				
			||||||
 | 
					    const hasValidExtension = allowedExtensions.some(ext => fileName.endsWith(ext));
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if (!hasValidExtension) {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        valid: false,
 | 
				
			||||||
 | 
					        error: `File type not allowed. Allowed: ${allowedExtensions.join(', ')}`
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return { valid: true };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async uploadFile(fileId: string) {
 | 
					  private async uploadFile(fileId: string) {
 | 
				
			||||||
    const fileItem = this.uploadedFiles.find(f => f.id === fileId);
 | 
					    const fileItem = this.uploadedFiles.find(f => f.id === fileId);
 | 
				
			||||||
    if (!fileItem) return;
 | 
					    if (!fileItem) {
 | 
				
			||||||
 | 
					      console.error('[UPLOAD] File item not found for ID:', fileId);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log('[UPLOAD] Starting upload for:', fileItem.name, 'Size:', fileItem.file.size, 'Type:', fileItem.file.type);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const formData = new FormData();
 | 
					    const formData = new FormData();
 | 
				
			||||||
    formData.append('file', fileItem.file);
 | 
					    formData.append('file', fileItem.file);
 | 
				
			||||||
    formData.append('type', 'knowledgebase');
 | 
					    formData.append('type', 'knowledgebase');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
 | 
					      console.log('[UPLOAD] Sending request to /api/upload/media');
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
      const response = await fetch('/api/upload/media', {
 | 
					      const response = await fetch('/api/upload/media', {
 | 
				
			||||||
        method: 'POST',
 | 
					        method: 'POST',
 | 
				
			||||||
        body: formData
 | 
					        body: formData
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      console.log('[UPLOAD] Response status:', response.status);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      let responseText: string;
 | 
				
			||||||
 | 
					      let responseData: any;
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        responseText = await response.text();
 | 
				
			||||||
 | 
					        console.log('[UPLOAD] Raw response:', responseText.substring(0, 200));
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          responseData = JSON.parse(responseText);
 | 
				
			||||||
 | 
					        } catch (parseError) {
 | 
				
			||||||
 | 
					          responseData = { error: responseText };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (readError) {
 | 
				
			||||||
 | 
					        console.error('[UPLOAD] Failed to read response:', readError);
 | 
				
			||||||
 | 
					        throw new Error('Failed to read server response');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (response.ok) {
 | 
					      if (response.ok) {
 | 
				
			||||||
        const result = await response.json();
 | 
					        console.log('[UPLOAD] Success result:', responseData);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        fileItem.uploaded = true;
 | 
					        fileItem.uploaded = true;
 | 
				
			||||||
        fileItem.url = result.url;
 | 
					        fileItem.url = responseData.url;
 | 
				
			||||||
        this.renderFileList();
 | 
					        this.renderFileList();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        throw new Error('Upload failed');
 | 
					        
 | 
				
			||||||
 | 
					        if (responseData && responseData.details) {
 | 
				
			||||||
 | 
					          console.error('[UPLOAD] Error details:', responseData.details);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      this.showMessage('error', `Failed to upload ${fileItem.name}`);
 | 
					      console.error('[UPLOAD] Upload error for', fileItem.name, ':', error);
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      const errorMessage = error instanceof Error 
 | 
				
			||||||
 | 
					        ? error.message 
 | 
				
			||||||
 | 
					        : 'Unknown upload error';
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
      this.removeFile(fileId);
 | 
					      this.removeFile(fileId);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -412,7 +493,6 @@ class KnowledgebaseForm {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error('[KB FORM] Submission error:', error);
 | 
					      console.error('[KB FORM] Submission error:', error);
 | 
				
			||||||
      this.showMessage('error', 'Submission failed. Please try again.');
 | 
					 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
      this.isSubmitting = false;
 | 
					      this.isSubmitting = false;
 | 
				
			||||||
      (this.elements.submitBtn as HTMLButtonElement).disabled = false;
 | 
					      (this.elements.submitBtn as HTMLButtonElement).disabled = false;
 | 
				
			||||||
@ -441,18 +521,6 @@ class KnowledgebaseForm {
 | 
				
			|||||||
    this.renderFileList();
 | 
					    this.renderFileList();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private showMessage(type: 'success' | 'error' | 'warning', message: string) {
 | 
					 | 
				
			||||||
    const container = document.getElementById('message-container');
 | 
					 | 
				
			||||||
    if (!container) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const messageEl = document.createElement('div');
 | 
					 | 
				
			||||||
    messageEl.className = `message message-${type}`;
 | 
					 | 
				
			||||||
    messageEl.textContent = message;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    container.appendChild(messageEl);
 | 
					 | 
				
			||||||
    setTimeout(() => messageEl.remove(), 5000);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public removeFileById(fileId: string) {
 | 
					  public removeFileById(fileId: string) {
 | 
				
			||||||
    this.removeFile(fileId);
 | 
					    this.removeFile(fileId);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user