forensic-pathways/src/pages/contribute/knowledgebase.astro
2025-07-25 23:23:38 +02:00

480 lines
16 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
// 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';
export const prerender = false;
// Check authentication
const authResult = await withAuth(Astro);
if (authResult instanceof Response) {
return authResult;
}
const { authenticated, userEmail, userId } = authResult;
// Load tools for reference (optional dropdown)
const data = await getToolsData();
const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.name));
---
<BaseLayout title="Contribute Knowledge Base Article">
<div class="container" style="max-width: 900px; margin: 0 auto; padding: 2rem 1rem;">
<!-- Header -->
<div class="hero-section">
<h1>Submit Knowledge Base Article</h1>
<p>Share documentation, tutorials, or insights about DFIR tools and methods. Your contribution will be submitted as an issue for maintainer review.</p>
{userEmail && <p><strong>Submitting as:</strong> {userEmail}</p>}
</div>
<!-- Main Form -->
<div class="card">
<form id="kb-form" novalidate>
<!-- Basic Information -->
<div class="form-section">
<h3 class="section-title">Basic Information</h3>
<div class="form-grid-2">
<div class="form-group">
<label for="tool-name" class="form-label">Related Tool (Optional)</label>
<select id="tool-name" name="toolName" class="form-input">
<option value="">Select a tool...</option>
{sortedTools.map(tool => (
<option value={tool.name}>{tool.name} ({tool.type})</option>
))}
</select>
</div>
<div class="form-group">
<label for="difficulty" class="form-label">Difficulty Level (Optional)</label>
<select id="difficulty" name="difficulty" class="form-input">
<option value="">Select difficulty...</option>
<option value="novice">Novice</option>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
<option value="expert">Expert</option>
</select>
</div>
</div>
<div class="form-group">
<label for="title" class="form-label">Article Title (Optional)</label>
<input
type="text"
id="title"
name="title"
maxlength="100"
placeholder="Clear, descriptive title for your article"
class="form-input"
/>
</div>
<div class="form-group">
<label for="description" class="form-label">Description (Optional)</label>
<textarea
id="description"
name="description"
maxlength="300"
rows="3"
placeholder="Brief summary of what this article covers"
class="form-input"
></textarea>
</div>
</div>
<!-- Content -->
<div class="form-section">
<h3 class="section-title">Content</h3>
<div class="form-group">
<label for="content" class="form-label">Article Content (Optional)</label>
<textarea
id="content"
name="content"
rows="8"
placeholder="Provide your content, documentation, tutorial steps, or notes here..."
class="form-input"
></textarea>
</div>
<div class="form-group">
<label for="external-link" class="form-label">External Link (Optional)</label>
<input
type="url"
id="external-link"
name="externalLink"
placeholder="https://example.com/documentation"
class="form-input"
/>
</div>
</div>
<!-- File Upload -->
<div class="form-section">
<h3 class="section-title">Upload Files</h3>
<div class="form-group">
<label class="form-label">Documents, Images, Videos (Optional)</label>
<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" style="display: none;">
<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"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
<p>Click to select files or drag & drop</p>
<small>PDFs, documents, images, archives, etc.</small>
</div>
</div>
<div id="file-list" class="file-list" style="display: none;">
<h5>Selected Files</h5>
<div id="files-container"></div>
</div>
</div>
</div>
<!-- Additional Information -->
<div class="form-section">
<h3 class="section-title">Additional Information</h3>
<div class="form-grid-2">
<div class="form-group">
<label for="categories" class="form-label">Categories (Optional)</label>
<input
type="text"
id="categories"
name="categories"
placeholder="setup, configuration, troubleshooting"
class="form-input"
/>
<small class="form-help">Comma-separated categories</small>
</div>
<div class="form-group">
<label for="tags" class="form-label">Tags (Optional)</label>
<input
type="text"
id="tags"
name="tags"
placeholder="installation, docker, linux, windows"
class="form-input"
/>
<small class="form-help">Comma-separated tags</small>
</div>
</div>
<div class="form-group">
<label for="reason" class="form-label">Reason for Contribution (Optional)</label>
<textarea
id="reason"
name="reason"
rows="3"
placeholder="Why are you submitting this article? What problem does it solve?"
class="form-input"
></textarea>
</div>
</div>
<!-- Submit Button -->
<div class="form-actions">
<a href="/" class="btn btn-secondary">Cancel</a>
<button type="submit" id="submit-btn" class="btn btn-accent">
<span id="submit-text">Submit Article</span>
<span id="submit-spinner" style="display: none;">⏳</span>
</button>
</div>
</form>
</div>
<!-- Success Modal -->
<div id="success-modal"
style="display:none; position:fixed; top:0; left:0; width:100%; height:100%;
background:rgba(0,0,0,.5); z-index:1000; align-items:center; justify-content:center;">
<div class="card" style="max-width:500px; width:90%; margin:2rem; text-align:center;">
<div style="font-size:3rem; margin-bottom:1rem;">✅</div>
<h3 style="margin-bottom:1rem;">Article Submitted!</h3>
<p id="success-message" style="margin-bottom:1.5rem;">
Your knowledgebase article has been submitted as an issue for review by maintainers.
</p>
<div style="display:flex; gap:1rem; justify-content:center;">
<a id="issue-link" href="#" target="_blank" class="btn btn-primary" style="display:none;">View Issue</a>
<a href="/" class="btn btn-secondary">Back to Home</a>
</div>
</div>
</div>
<!-- Message Container -->
<div id="message-container" class="message-container"></div>
</div>
</BaseLayout>
<script>
interface UploadedFile {
id: string;
file: File;
name: string;
uploaded: boolean;
url?: string;
}
declare global {
interface Window {
removeFile: (fileId: string) => void;
}
}
class KnowledgebaseForm {
private uploadedFiles: UploadedFile[] = [];
private isSubmitting = false;
private elements: Record<string, HTMLElement | null> = {};
constructor() {
this.init();
}
private init() {
// Get elements
this.elements = {
form: document.getElementById('kb-form'),
submitBtn: document.getElementById('submit-btn'),
submitText: document.getElementById('submit-text'),
submitSpinner: document.getElementById('submit-spinner'),
fileInput: document.getElementById('file-input'),
uploadArea: document.getElementById('upload-area'),
fileList: document.getElementById('file-list'),
filesContainer: document.getElementById('files-container'),
successModal: document.getElementById('success-modal')
};
if (!this.elements.form || !this.elements.submitBtn) {
console.error('[KB FORM] Critical elements missing');
return;
}
this.setupEventListeners();
this.setupFileUpload();
}
private setupEventListeners() {
// Form submission
this.elements.form?.addEventListener('submit', (e) => {
e.preventDefault();
if (!this.isSubmitting) {
this.handleSubmit();
}
});
}
private setupFileUpload() {
if (!this.elements.fileInput || !this.elements.uploadArea) return;
this.elements.uploadArea.addEventListener('click', () => {
(this.elements.fileInput as HTMLInputElement)?.click();
});
this.elements.uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
this.elements.uploadArea?.classList.add('drag-over');
});
this.elements.uploadArea.addEventListener('dragleave', () => {
this.elements.uploadArea?.classList.remove('drag-over');
});
this.elements.uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
this.elements.uploadArea?.classList.remove('drag-over');
if (e.dataTransfer?.files) {
this.handleFiles(Array.from(e.dataTransfer.files));
}
});
this.elements.fileInput.addEventListener('change', (e) => {
const target = e.target as HTMLInputElement;
if (target?.files) {
this.handleFiles(Array.from(target.files));
}
});
}
private handleFiles(files: File[]) {
files.forEach(file => {
const fileId = Date.now() + '-' + Math.random().toString(36).substr(2, 9);
const newFile: UploadedFile = {
id: fileId,
file,
name: file.name,
uploaded: false
};
this.uploadedFiles.push(newFile);
this.uploadFile(fileId);
});
this.renderFileList();
}
private async uploadFile(fileId: string) {
const fileItem = this.uploadedFiles.find(f => f.id === fileId);
if (!fileItem) return;
const formData = new FormData();
formData.append('file', fileItem.file);
formData.append('type', 'knowledgebase');
try {
const response = await fetch('/api/upload/media', {
method: 'POST',
body: formData
});
if (response.ok) {
const result = await response.json();
fileItem.uploaded = true;
fileItem.url = result.url;
this.renderFileList();
} else {
throw new Error('Upload failed');
}
} catch (error) {
this.showMessage('error', `Failed to upload ${fileItem.name}`);
this.removeFile(fileId);
}
}
private removeFile(fileId: string) {
this.uploadedFiles = this.uploadedFiles.filter(f => f.id !== fileId);
this.renderFileList();
}
private renderFileList() {
if (!this.elements.filesContainer || !this.elements.fileList) return;
if (this.uploadedFiles.length > 0) {
(this.elements.fileList as HTMLElement).style.display = 'block';
(this.elements.filesContainer as HTMLElement).innerHTML = this.uploadedFiles.map(file => `
<div class="file-item">
<div class="file-info">
<strong>${file.name}</strong>
<div class="file-meta">
${(file.file.size / 1024 / 1024).toFixed(2)} MB
${file.uploaded ?
'<span class="file-status success">✓ Uploaded</span>' :
'<span class="file-status pending">⏳ Uploading...</span>'
}
</div>
</div>
<button type="button" onclick="window.removeFile('${file.id}')" class="btn btn-danger btn-small">Remove</button>
</div>
`).join('');
} else {
(this.elements.fileList as HTMLElement).style.display = 'none';
}
}
private async handleSubmit() {
if (this.isSubmitting) return;
this.isSubmitting = true;
// Update UI
(this.elements.submitBtn as HTMLButtonElement).disabled = true;
(this.elements.submitText as HTMLElement).textContent = 'Submitting...';
(this.elements.submitSpinner as HTMLElement).style.display = 'inline';
try {
const formData = new FormData(this.elements.form as HTMLFormElement);
// Process categories and tags
const categoriesValue = (formData.get('categories') as string) || '';
const tagsValue = (formData.get('tags') as string) || '';
const categories = categoriesValue.split(',').map(s => s.trim()).filter(s => s);
const tags = tagsValue.split(',').map(s => s.trim()).filter(s => s);
formData.set('categories', JSON.stringify(categories));
formData.set('tags', JSON.stringify(tags));
// Add uploaded files
formData.set('uploadedFiles', JSON.stringify(this.uploadedFiles.filter(f => f.uploaded)));
const response = await fetch('/api/contribute/knowledgebase', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
this.showSuccess(result);
} else {
throw new Error(result.error || 'Submission failed');
}
} 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;
(this.elements.submitText as HTMLElement).textContent = 'Submit Article';
(this.elements.submitSpinner as HTMLElement).style.display = 'none';
}
}
private showSuccess(result: any) {
const successMessage = document.getElementById('success-message');
const issueLink = document.getElementById('issue-link') as HTMLAnchorElement;
if (successMessage) {
successMessage.textContent = 'Your knowledge base article has been submitted as an issue for review by maintainers.';
}
if (result.issueUrl && issueLink) {
issueLink.href = result.issueUrl;
issueLink.style.display = 'inline-flex';
}
(this.elements.successModal as HTMLElement).style.display = 'flex';
// Reset form
(this.elements.form as HTMLFormElement).reset();
this.uploadedFiles = [];
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 method for file removal
public removeFileById(fileId: string) {
this.removeFile(fileId);
}
}
// Global instance
let formInstance: KnowledgebaseForm;
// Global function for file removal
window.removeFile = (fileId: string) => {
if (formInstance) {
formInstance.removeFileById(fileId);
}
};
// Initialize form
document.addEventListener('DOMContentLoaded', () => {
formInstance = new KnowledgebaseForm();
});
</script>