navidocs/server/routes/documents.js
ggq-admin 155a8c0305 feat: NaviDocs MVP - Complete codebase extraction from lilian1
## Backend (server/)
- Express 5 API with security middleware (helmet, rate limiting)
- SQLite database with WAL mode (schema from docs/architecture/)
- Meilisearch integration with tenant tokens
- BullMQ + Redis background job queue
- OCR pipeline with Tesseract.js
- File safety validation (extension, MIME, size)
- 4 API route modules: upload, jobs, search, documents

## Frontend (client/)
- Vue 3 with Composition API (<script setup>)
- Vite 5 build system with HMR
- Tailwind CSS (Meilisearch-inspired design)
- UploadModal with drag-and-drop
- FigureZoom component (ported from lilian1)
- Meilisearch search integration with tenant tokens
- Job polling composable
- Clean SVG icons (no emojis)

## Code Extraction
-  manuals.js → UploadModal.vue, useJobPolling.js
-  figure-zoom.js → FigureZoom.vue
-  service-worker.js → client/public/service-worker.js (TODO)
-  glossary.json → Merged into Meilisearch synonyms
-  Discarded: quiz.js, persona.js, gamification.js (Frank-AI junk)

## Documentation
- Complete extraction plan in docs/analysis/
- README with quick start guide
- Architecture summary in docs/architecture/

## Build Status
- Server dependencies:  Installed (234 packages)
- Client dependencies:  Installed (160 packages)
- Client build:  Successful (2.63s)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 01:55:44 +02:00

360 lines
8.9 KiB
JavaScript

/**
* Documents Route - GET /api/documents/:id
* Query document metadata with ownership verification
*/
import express from 'express';
import { getDb } from '../db/db.js';
const router = express.Router();
/**
* GET /api/documents/:id
* Get document metadata and page information
*
* @param {string} id - Document UUID
* @returns {Object} Document metadata with pages
*/
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
// Validate UUID format (basic check)
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(id)) {
return res.status(400).json({ error: 'Invalid document ID format' });
}
// TODO: Authentication middleware should provide req.user
const userId = req.user?.id || 'test-user-id';
const db = getDb();
// Query document with ownership check
const document = db.prepare(`
SELECT
d.id,
d.organization_id,
d.entity_id,
d.sub_entity_id,
d.component_id,
d.uploaded_by,
d.title,
d.document_type,
d.file_path,
d.file_name,
d.file_size,
d.mime_type,
d.page_count,
d.language,
d.status,
d.created_at,
d.updated_at,
d.metadata
FROM documents d
WHERE d.id = ?
`).get(id);
if (!document) {
return res.status(404).json({ error: 'Document not found' });
}
// Verify ownership or organization membership
const hasAccess = db.prepare(`
SELECT 1 FROM user_organizations
WHERE user_id = ? AND organization_id = ?
UNION
SELECT 1 FROM documents
WHERE id = ? AND uploaded_by = ?
UNION
SELECT 1 FROM document_shares
WHERE document_id = ? AND shared_with = ?
`).get(userId, document.organization_id, id, userId, id, userId);
if (!hasAccess) {
return res.status(403).json({
error: 'Access denied',
message: 'You do not have permission to view this document'
});
}
// Get page information
const pages = db.prepare(`
SELECT
id,
page_number,
ocr_confidence,
ocr_language,
ocr_completed_at,
search_indexed_at
FROM document_pages
WHERE document_id = ?
ORDER BY page_number ASC
`).all(id);
// Get entity information if linked
let entity = null;
if (document.entity_id) {
entity = db.prepare(`
SELECT id, name, entity_type
FROM entities
WHERE id = ?
`).get(document.entity_id);
}
// Get component information if linked
let component = null;
if (document.component_id) {
component = db.prepare(`
SELECT id, name, manufacturer, model_number
FROM components
WHERE id = ?
`).get(document.component_id);
}
// Parse metadata JSON if exists
let metadata = null;
if (document.metadata) {
try {
metadata = JSON.parse(document.metadata);
} catch (e) {
console.error('Error parsing document metadata:', e);
}
}
// Build response
const response = {
id: document.id,
organizationId: document.organization_id,
entityId: document.entity_id,
subEntityId: document.sub_entity_id,
componentId: document.component_id,
uploadedBy: document.uploaded_by,
title: document.title,
documentType: document.document_type,
fileName: document.file_name,
fileSize: document.file_size,
mimeType: document.mime_type,
pageCount: document.page_count,
language: document.language,
status: document.status,
createdAt: document.created_at,
updatedAt: document.updated_at,
metadata,
filePath: document.file_path, // For PDF serving (should be restricted in production)
pages: pages.map(page => ({
id: page.id,
pageNumber: page.page_number,
ocrConfidence: page.ocr_confidence,
ocrLanguage: page.ocr_language,
ocrCompletedAt: page.ocr_completed_at,
searchIndexedAt: page.search_indexed_at
})),
entity,
component
};
res.json(response);
} catch (error) {
console.error('Document retrieval error:', error);
res.status(500).json({
error: 'Failed to retrieve document',
message: error.message
});
}
});
/**
* GET /api/documents
* List documents with optional filtering
* Query params: organizationId, entityId, documentType, status, limit, offset
*/
router.get('/', async (req, res) => {
try {
const {
organizationId,
entityId,
documentType,
status,
limit = 50,
offset = 0
} = req.query;
// TODO: Authentication middleware should provide req.user
const userId = req.user?.id || 'test-user-id';
const db = getDb();
// Build query with filters
let query = `
SELECT
d.id,
d.organization_id,
d.entity_id,
d.title,
d.document_type,
d.file_name,
d.file_size,
d.page_count,
d.status,
d.created_at,
d.updated_at
FROM documents d
INNER JOIN user_organizations uo ON d.organization_id = uo.organization_id
WHERE uo.user_id = ?
`;
const params = [userId];
if (organizationId) {
query += ' AND d.organization_id = ?';
params.push(organizationId);
}
if (entityId) {
query += ' AND d.entity_id = ?';
params.push(entityId);
}
if (documentType) {
query += ' AND d.document_type = ?';
params.push(documentType);
}
if (status) {
query += ' AND d.status = ?';
params.push(status);
}
query += ' ORDER BY d.created_at DESC LIMIT ? OFFSET ?';
params.push(parseInt(limit), parseInt(offset));
const documents = db.prepare(query).all(...params);
// Get total count for pagination
let countQuery = `
SELECT COUNT(*) as total
FROM documents d
INNER JOIN user_organizations uo ON d.organization_id = uo.organization_id
WHERE uo.user_id = ?
`;
const countParams = [userId];
if (organizationId) {
countQuery += ' AND d.organization_id = ?';
countParams.push(organizationId);
}
if (entityId) {
countQuery += ' AND d.entity_id = ?';
countParams.push(entityId);
}
if (documentType) {
countQuery += ' AND d.document_type = ?';
countParams.push(documentType);
}
if (status) {
countQuery += ' AND d.status = ?';
countParams.push(status);
}
const { total } = db.prepare(countQuery).get(...countParams);
res.json({
documents: documents.map(doc => ({
id: doc.id,
organizationId: doc.organization_id,
entityId: doc.entity_id,
title: doc.title,
documentType: doc.document_type,
fileName: doc.file_name,
fileSize: doc.file_size,
pageCount: doc.page_count,
status: doc.status,
createdAt: doc.created_at,
updatedAt: doc.updated_at
})),
pagination: {
total,
limit: parseInt(limit),
offset: parseInt(offset),
hasMore: parseInt(offset) + documents.length < total
}
});
} catch (error) {
console.error('Documents list error:', error);
res.status(500).json({
error: 'Failed to retrieve documents',
message: error.message
});
}
});
/**
* DELETE /api/documents/:id
* Soft delete a document (mark as deleted)
*/
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
// TODO: Authentication middleware should provide req.user
const userId = req.user?.id || 'test-user-id';
const db = getDb();
// Check ownership
const document = db.prepare(`
SELECT id, organization_id, uploaded_by
FROM documents
WHERE id = ?
`).get(id);
if (!document) {
return res.status(404).json({ error: 'Document not found' });
}
// Verify user has permission (must be uploader or org admin)
const hasPermission = db.prepare(`
SELECT 1 FROM user_organizations
WHERE user_id = ? AND organization_id = ? AND role IN ('admin', 'manager')
UNION
SELECT 1 FROM documents
WHERE id = ? AND uploaded_by = ?
`).get(userId, document.organization_id, id, userId);
if (!hasPermission) {
return res.status(403).json({
error: 'Access denied',
message: 'You do not have permission to delete this document'
});
}
// Soft delete - update status
const timestamp = Date.now();
db.prepare(`
UPDATE documents
SET status = 'deleted', updated_at = ?
WHERE id = ?
`).run(timestamp, id);
res.json({
message: 'Document deleted successfully',
documentId: id
});
} catch (error) {
console.error('Document deletion error:', error);
res.status(500).json({
error: 'Failed to delete document',
message: error.message
});
}
});
export default router;