Compare commits

..

5 Commits

Author SHA1 Message Date
6e9f37c1cd Merge pull request 'upload-issue' (#9) from upload-issue into main
Reviewed-on: #9
2025-08-09 13:23:23 +00:00
overcuriousity
7607a73373 simplify 2025-08-09 15:22:11 +02:00
overcuriousity
3f9d1860aa uploads fix 2025-08-09 15:02:20 +02:00
overcuriousity
daa468c535 uploadFile 2025-08-09 10:42:59 +02:00
overcuriousity
1d10bfca2c more error handling and logging in uploads mechanic 2025-08-09 10:30:11 +02:00
4 changed files with 210 additions and 41 deletions

File diff suppressed because one or more lines are too long

View File

@ -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;

View File

@ -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');

View File

@ -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);
} }