From a6d445f36a6bb98c03696c25b35ff715df504013 Mon Sep 17 00:00:00 2001 From: Santhosh Janardhanan Date: Tue, 27 Jan 2026 14:42:26 -0500 Subject: [PATCH] iteration 1 complete --- env-sample | 29 +++++++ src/app.js | 135 +++++++++++++++++++++++++++++---- src/services/analysisWorker.js | 21 +++++ 3 files changed, 172 insertions(+), 13 deletions(-) create mode 100644 env-sample diff --git a/env-sample b/env-sample new file mode 100644 index 0000000..96d58d9 --- /dev/null +++ b/env-sample @@ -0,0 +1,29 @@ +# Database +DATABASE_URL=postgresql://postgres:changeme@postgres:5432/privacy_analyzer + +# Redis +REDIS_URL=redis://redis:6379 + +# Meilisearch +MEILISEARCH_URL=http://meilisearch:7700 +MEILISEARCH_API_KEY=your_secure_master_key_here + +# AI Provider Configuration +# Option 1: Ollama (Local LLM - DEFAULT, no API costs!) +USE_OLLAMA=true +OLLAMA_URL=http://ollama:11434 +OLLAMA_MODEL=gpt-oss:latest + +# Option 2: OpenAI (Cloud - optional fallback) +# Set these if you want OpenAI as fallback when Ollama is unavailable +OPENAI_API_KEY=sk-proj- +OPENAI_MODEL=gpt-4o-mini + +# Admin Credentials (change these!) +ADMIN_USERNAME=admin +ADMIN_PASSWORD=secure_password_here +SESSION_SECRET=your_random_session_secret_here + +# App +PORT=3000 +NODE_ENV=local diff --git a/src/app.js b/src/app.js index b585f2e..c2bf5eb 100644 --- a/src/app.js +++ b/src/app.js @@ -151,18 +151,60 @@ async function handleRequest(req) { // Search - GET /search if (method === 'GET' && pathname === '/search') { const query = url.searchParams.get('q'); - const services = await Service.findAllWithLatestAnalysis(); - const html = await renderTemplate('public/index', { - title: query ? `Search Results for "${query}" | Privacy Policy Analyzer` : 'Search Services', - description: 'Search for services and see their privacy grades.', - canonical: `${req.protocol}://${url.host}/search`, - services - }); - - return new Response(html, { - headers: { 'Content-Type': 'text/html' } - }); + try { + let services; + + if (query && query.trim().length > 0) { + // Use Meilisearch for search + console.log(`Searching for: ${query}`); + const searchResults = await SearchIndexer.search(query, { limit: 50 }); + services = searchResults.hits.map(hit => ({ + id: hit.id, + name: hit.name, + url: hit.url, + logo_url: hit.logo_url, + grade: hit.grade, + overall_score: hit.overall_score, + findings: hit.findings, + last_analyzed: hit.last_analyzed, + created_at: hit.created_at + })); + } else { + // Show all services if no query + services = await Service.findAllWithLatestAnalysis(); + } + + const html = await renderTemplate('public/index', { + title: query ? `Search Results for "${query}" | Privacy Policy Analyzer` : 'Search Services', + description: 'Search for services and see their privacy grades.', + canonical: `${req.protocol}://${url.host}/search`, + services, + query, + pagination: null // Search results don't use pagination + }); + + return new Response(html, { + headers: { 'Content-Type': 'text/html' } + }); + } catch (error) { + console.error('Search error:', error); + // Fallback to showing all services + const services = await Service.findAllWithLatestAnalysis(); + + const html = await renderTemplate('public/index', { + title: 'Search Services', + description: 'Search for services and see their privacy grades.', + canonical: `${req.protocol}://${url.host}/search`, + services, + error: 'Search temporarily unavailable', + pagination: null + }); + + return new Response(html, { + headers: { 'Content-Type': 'text/html' } + }); + } } // Admin login page - GET /admin/login @@ -201,7 +243,7 @@ async function handleRequest(req) { const headers = new Headers(); headers.set('Content-Type', 'application/json'); - headers.set('Set-Cookie', `session_token=${session.session_token}; HttpOnly; Path=/; Max-Age=${24 * 60 * 60}`); + headers.set('Set-Cookie', `session_token=${session.session_token}; HttpOnly; Path=/; Max-Age=${24 * 60 * 60}; SameSite=Lax`); return new Response(JSON.stringify({ success: true, @@ -249,6 +291,9 @@ async function handleRequest(req) { return new Response(null, { status: 302, headers }); } + // Extend session on each request + await AdminSession.extendSession(sessionToken); + // Clean up expired sessions await AdminSession.deleteExpired(); @@ -354,9 +399,17 @@ async function handleRequest(req) { return new Response('Name, URL, and policy URL are required', { status: 400 }); } - await Service.create(data); + const service = await Service.create(data); console.log('Service created successfully'); + // Index in Meilisearch + try { + await SearchIndexer.indexService(service); + console.log('Service indexed in Meilisearch'); + } catch (indexError) { + console.error('Failed to index service:', indexError); + } + // Invalidate homepage cache await PageCache.invalidateHomepage(); @@ -436,6 +489,15 @@ async function handleRequest(req) { await Service.update(id, data); console.log('Service updated successfully'); + // Re-index in Meilisearch + try { + const updatedService = await Service.findById(id); + await SearchIndexer.indexService(updatedService); + console.log('Service re-indexed in Meilisearch'); + } catch (indexError) { + console.error('Failed to re-index service:', indexError); + } + // Invalidate caches await PageCache.invalidateHomepage(); await PageCache.invalidateService(id); @@ -467,6 +529,14 @@ async function handleRequest(req) { const id = parseInt(match[1]); await Service.delete(id); + // Delete from Meilisearch + try { + await SearchIndexer.deleteService(id); + console.log('Service deleted from Meilisearch'); + } catch (indexError) { + console.error('Failed to delete service from index:', indexError); + } + // Invalidate caches await PageCache.invalidateHomepage(); await PageCache.invalidateService(id); @@ -580,6 +650,36 @@ async function handleRequest(req) { } } + // Admin reindex all services - POST /api/admin/reindex + if (method === 'POST' && pathname === '/api/admin/reindex') { + const sessionToken = req.cookies?.session_token; + + if (!sessionToken || !(await AdminSession.findByToken(sessionToken))) { + return new Response(JSON.stringify({ error: 'Unauthorized' }), { + status: 401, + headers: { 'Content-Type': 'application/json' } + }); + } + + try { + console.log('Starting bulk reindex...'); + const result = await SearchIndexer.indexAll(); + return new Response(JSON.stringify({ + success: true, + message: `Indexed ${result.indexed} services`, + indexed: result.indexed + }), { + headers: { 'Content-Type': 'application/json' } + }); + } catch (error) { + console.error('Reindex error:', error); + return new Response(JSON.stringify({ error: error.message }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } + } + // Sitemap.xml - GET /sitemap.xml if (method === 'GET' && pathname === '/sitemap.xml') { try { @@ -684,6 +784,15 @@ console.log(`Server running at http://localhost:${server.port}`); await SearchIndexer.init(); console.log('Search indexer initialized'); + // Index all existing services on startup + try { + console.log('Indexing all services on startup...'); + const result = await SearchIndexer.indexAll(); + console.log(`Indexed ${result.indexed} services on startup`); + } catch (indexError) { + console.error('Startup indexing error:', indexError.message); + } + // Start analysis worker AnalysisWorker.start(); console.log('Analysis worker started'); diff --git a/src/services/analysisWorker.js b/src/services/analysisWorker.js index 91c25a6..525a4c2 100644 --- a/src/services/analysisWorker.js +++ b/src/services/analysisWorker.js @@ -123,6 +123,27 @@ export class AnalysisWorker { console.log(`[${jobId}] Analysis complete: Grade ${analysis.overall_score}`); + // Index in Meilisearch + try { + const serviceWithAnalysis = { + id: serviceId, + name: service.name, + url: service.url, + logo_url: service.logo_url, + grade: analysis.overall_score, + overall_score: analysis.overall_score, + findings: analysisResult.findings, + data_types_collected: analysisResult.data_types_collected || [], + third_parties: analysisResult.third_parties || [], + last_analyzed: analysis.created_at, + created_at: service.created_at + }; + await SearchIndexer.indexService(serviceWithAnalysis); + console.log(`[${jobId}] Service indexed in Meilisearch`); + } catch (indexError) { + console.error(`[${jobId}] Meilisearch indexing error:`, indexError.message); + } + // Invalidate caches try { await PageCache.invalidateHomepage();