Compare commits
No commits in common. "6e9f37c1cdd7e600ad579415d77a9a2992d2cdd8" and "8aa9a9b08221a4faa6ee1119ad390cc8d487a31c" have entirely different histories.
6e9f37c1cd
...
8aa9a9b082
File diff suppressed because one or more lines are too long
@ -142,7 +142,6 @@ WantedBy=multi-user.target
|
||||
server {
|
||||
listen 80;
|
||||
server_name forensic-pathways.yourdomain.com;
|
||||
client_max_body_size 50M; # Important for uploads
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:4321;
|
||||
|
@ -1,4 +1,4 @@
|
||||
// src/pages/api/upload/media.ts - Enhanced with detailed logging and error handling
|
||||
// src/pages/api/upload/media.ts
|
||||
import type { APIRoute } from 'astro';
|
||||
import { withAPIAuth } from '../../../utils/auth.js';
|
||||
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
|
||||
@ -21,13 +21,8 @@ interface UploadResult {
|
||||
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',
|
||||
@ -35,32 +30,7 @@ const UPLOAD_CONFIG = {
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
|
||||
// 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'
|
||||
'text/plain', 'text/csv', 'application/json'
|
||||
]),
|
||||
localUploadPath: process.env.LOCAL_UPLOAD_PATH || './public/uploads',
|
||||
publicBaseUrl: process.env.PUBLIC_BASE_URL || 'http://localhost:4321'
|
||||
@ -80,7 +50,6 @@ function checkUploadRateLimit(userEmail: string): boolean {
|
||||
}
|
||||
|
||||
if (userLimit.count >= RATE_LIMIT_MAX) {
|
||||
console.warn(`[UPLOAD] Rate limit exceeded for user: ${userEmail} (${userLimit.count}/${RATE_LIMIT_MAX})`);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -89,37 +58,27 @@ function checkUploadRateLimit(userEmail: string): boolean {
|
||||
}
|
||||
|
||||
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) {
|
||||
const errorMsg = `File too large. Maximum size is ${Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024)}MB`;
|
||||
console.warn(`[UPLOAD] ${errorMsg} - File size: ${file.size}`);
|
||||
return { valid: false, error: errorMsg };
|
||||
return {
|
||||
valid: false,
|
||||
error: `File too large. Maximum size is ${Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024)}MB`
|
||||
};
|
||||
}
|
||||
|
||||
if (!UPLOAD_CONFIG.allowedTypes.has(file.type)) {
|
||||
const errorMsg = `File type ${file.type} not allowed`;
|
||||
console.warn(`[UPLOAD] ${errorMsg} - Allowed types:`, Array.from(UPLOAD_CONFIG.allowedTypes));
|
||||
return { valid: false, error: errorMsg };
|
||||
return {
|
||||
valid: false,
|
||||
error: `File type ${file.type} not allowed`
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`[UPLOAD] File validation passed for: ${file.name}`);
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadResult> {
|
||||
console.log(`[UPLOAD] Attempting Nextcloud upload for: ${file.name} by ${userEmail}`);
|
||||
|
||||
try {
|
||||
const uploader = new NextcloudUploader();
|
||||
const result = await uploader.uploadFile(file, userEmail);
|
||||
|
||||
console.log(`[UPLOAD] Nextcloud upload successful:`, {
|
||||
filename: result.filename,
|
||||
url: result.url,
|
||||
size: file.size
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
url: result.url,
|
||||
@ -128,7 +87,7 @@ async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadR
|
||||
storage: 'nextcloud'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[UPLOAD] Nextcloud upload failed:', error);
|
||||
console.error('Nextcloud upload failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Nextcloud upload failed',
|
||||
@ -138,10 +97,7 @@ async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadR
|
||||
}
|
||||
|
||||
async function uploadToLocal(file: File, userType: string): Promise<UploadResult> {
|
||||
console.log(`[UPLOAD] Attempting local upload for: ${file.name} (${userType})`);
|
||||
|
||||
try {
|
||||
console.log(`[UPLOAD] Creating directory: ${UPLOAD_CONFIG.localUploadPath}`);
|
||||
await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
@ -150,20 +106,11 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
|
||||
const filename = `${timestamp}-${randomString}${extension}`;
|
||||
|
||||
const filepath = path.join(UPLOAD_CONFIG.localUploadPath, filename);
|
||||
console.log(`[UPLOAD] Writing file to: ${filepath}`);
|
||||
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
await fs.writeFile(filepath, buffer);
|
||||
|
||||
const publicUrl = `${UPLOAD_CONFIG.publicBaseUrl}/uploads/${filename}`;
|
||||
|
||||
console.log(`[UPLOAD] Local upload successful:`, {
|
||||
filename,
|
||||
filepath,
|
||||
publicUrl,
|
||||
size: file.size
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
url: publicUrl,
|
||||
@ -172,7 +119,7 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
|
||||
storage: 'local'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[UPLOAD] Local upload failed:', error);
|
||||
console.error('Local upload failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Local upload failed',
|
||||
@ -183,23 +130,12 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
return await handleAPIRequest(async () => {
|
||||
console.log('[UPLOAD] Processing upload request');
|
||||
|
||||
// Enhanced auth logging
|
||||
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) {
|
||||
console.warn('[UPLOAD] Upload rejected - authentication required but user not authenticated');
|
||||
return apiError.unauthorized('Authentication required for file uploads');
|
||||
return apiError.unauthorized();
|
||||
}
|
||||
|
||||
const userEmail = authResult.session?.email || 'anon@anon.anon';
|
||||
console.log(`[UPLOAD] Processing upload for user: ${userEmail}`);
|
||||
|
||||
if (!checkUploadRateLimit(userEmail)) {
|
||||
return apiError.rateLimit('Upload rate limit exceeded. Please wait before uploading again.');
|
||||
@ -207,60 +143,38 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
|
||||
let formData;
|
||||
try {
|
||||
console.log('[UPLOAD] Parsing form data');
|
||||
formData = await request.formData();
|
||||
console.log('[UPLOAD] Form data keys:', Array.from(formData.keys()));
|
||||
} catch (error) {
|
||||
console.error('[UPLOAD] Failed to parse form data:', error);
|
||||
return apiError.badRequest('Invalid form data - could not parse request');
|
||||
return apiError.badRequest('Invalid form data');
|
||||
}
|
||||
|
||||
const file = formData.get('file') as File;
|
||||
const type = formData.get('type') as string;
|
||||
|
||||
if (!file) {
|
||||
console.warn('[UPLOAD] No file provided in request');
|
||||
return apiSpecial.missingRequired(['file']);
|
||||
}
|
||||
|
||||
console.log(`[UPLOAD] Processing file: ${file.name}, type parameter: ${type}`);
|
||||
|
||||
const validation = validateFile(file);
|
||||
if (!validation.valid) {
|
||||
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;
|
||||
|
||||
if (nextcloudConfigured) {
|
||||
console.log('[UPLOAD] Using Nextcloud as primary storage');
|
||||
if (isNextcloudConfigured()) {
|
||||
result = await uploadToNextcloud(file, userEmail);
|
||||
|
||||
if (!result.success) {
|
||||
console.warn('[UPLOAD] Nextcloud upload failed, trying local fallback:', result.error);
|
||||
console.warn('Nextcloud upload failed, trying local fallback:', result.error);
|
||||
result = await uploadToLocal(file, type);
|
||||
}
|
||||
} else {
|
||||
console.log('[UPLOAD] Using local storage (Nextcloud not configured)');
|
||||
result = await uploadToLocal(file, type);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
console.log(`[UPLOAD] Upload completed successfully:`, {
|
||||
filename: result.filename,
|
||||
storage: result.storage,
|
||||
url: result.url,
|
||||
user: userEmail
|
||||
});
|
||||
console.log(`[MEDIA UPLOAD] ${file.name} (${file.size} bytes) by ${userEmail} -> ${result.storage}: ${result.url}`);
|
||||
|
||||
return apiSpecial.uploadSuccess({
|
||||
url: result.url!,
|
||||
@ -269,12 +183,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
storage: result.storage!
|
||||
});
|
||||
} else {
|
||||
console.error(`[UPLOAD] Upload failed completely:`, {
|
||||
filename: file.name,
|
||||
error: result.error,
|
||||
storage: result.storage,
|
||||
user: userEmail
|
||||
});
|
||||
console.error(`[MEDIA UPLOAD FAILED] ${file.name} by ${userEmail}: ${result.error}`);
|
||||
|
||||
return apiSpecial.uploadFailed(result.error!);
|
||||
}
|
||||
@ -284,8 +193,6 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
return await handleAPIRequest(async () => {
|
||||
console.log('[UPLOAD] Getting upload status');
|
||||
|
||||
const authResult = await withAPIAuth(request);
|
||||
if (authResult.authRequired && !authResult.authenticated) {
|
||||
return apiError.unauthorized();
|
||||
@ -297,14 +204,12 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
await fs.access(UPLOAD_CONFIG.localUploadPath);
|
||||
localStorageAvailable = true;
|
||||
console.log('[UPLOAD] Local storage accessible');
|
||||
} catch {
|
||||
try {
|
||||
await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
|
||||
localStorageAvailable = true;
|
||||
console.log('[UPLOAD] Local storage created');
|
||||
} catch (error) {
|
||||
console.warn('[UPLOAD] Local upload directory not accessible:', error);
|
||||
console.warn('Local upload directory not accessible:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -332,14 +237,9 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
paths: {
|
||||
uploadEndpoint: '/api/upload/media',
|
||||
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);
|
||||
|
||||
}, 'Upload status retrieval failed');
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
// src/pages/contribute/knowledgebase.astro
|
||||
// src/pages/contribute/knowledgebase.astro - SIMPLIFIED: Issues only, minimal validation
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { withAuth } from '../../utils/auth.js';
|
||||
import { getToolsData } from '../../utils/dataService.js';
|
||||
@ -114,13 +114,8 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
|
||||
<div class="form-group">
|
||||
<label class="form-label">Dokumente, Bilder, Videos (Optional)</label>
|
||||
<div class="upload-area" id="upload-area">
|
||||
<input
|
||||
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">
|
||||
<input type="file" id="file-input" multiple accept=".pdf,.doc,.docx,.txt,.md,.zip,.png,.jpg,.jpeg,.gif,.mp4,.webm" class="hidden">
|
||||
<div class="upload-placeholder">
|
||||
<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"/>
|
||||
<polyline points="7 10 12 15 17 10"/>
|
||||
@ -309,13 +304,6 @@ class KnowledgebaseForm {
|
||||
|
||||
private handleFiles(files: 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 newFile: UploadedFile = {
|
||||
id: fileId,
|
||||
@ -329,99 +317,30 @@ class KnowledgebaseForm {
|
||||
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) {
|
||||
const fileItem = this.uploadedFiles.find(f => f.id === fileId);
|
||||
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);
|
||||
if (!fileItem) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileItem.file);
|
||||
formData.append('type', 'knowledgebase');
|
||||
|
||||
try {
|
||||
console.log('[UPLOAD] Sending request to /api/upload/media');
|
||||
|
||||
const response = await fetch('/api/upload/media', {
|
||||
method: 'POST',
|
||||
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) {
|
||||
console.log('[UPLOAD] Success result:', responseData);
|
||||
|
||||
const result = await response.json();
|
||||
fileItem.uploaded = true;
|
||||
fileItem.url = responseData.url;
|
||||
fileItem.url = result.url;
|
||||
this.renderFileList();
|
||||
|
||||
} else {
|
||||
|
||||
if (responseData && responseData.details) {
|
||||
console.error('[UPLOAD] Error details:', responseData.details);
|
||||
}
|
||||
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[UPLOAD] Upload error for', fileItem.name, ':', error);
|
||||
|
||||
const errorMessage = error instanceof Error
|
||||
? error.message
|
||||
: 'Unknown upload error';
|
||||
|
||||
this.showMessage('error', `Failed to upload ${fileItem.name}`);
|
||||
this.removeFile(fileId);
|
||||
}
|
||||
}
|
||||
@ -493,6 +412,7 @@ class KnowledgebaseForm {
|
||||
|
||||
} catch (error) {
|
||||
console.error('[KB FORM] Submission error:', error);
|
||||
this.showMessage('error', 'Submission failed. Please try again.');
|
||||
} finally {
|
||||
this.isSubmitting = false;
|
||||
(this.elements.submitBtn as HTMLButtonElement).disabled = false;
|
||||
@ -521,6 +441,18 @@ class KnowledgebaseForm {
|
||||
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) {
|
||||
this.removeFile(fileId);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user