iteration 1 complete
This commit is contained in:
29
env-sample
Normal file
29
env-sample
Normal file
@@ -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
|
||||||
135
src/app.js
135
src/app.js
@@ -151,18 +151,60 @@ async function handleRequest(req) {
|
|||||||
// Search - GET /search
|
// Search - GET /search
|
||||||
if (method === 'GET' && pathname === '/search') {
|
if (method === 'GET' && pathname === '/search') {
|
||||||
const query = url.searchParams.get('q');
|
const query = url.searchParams.get('q');
|
||||||
const services = await Service.findAllWithLatestAnalysis();
|
|
||||||
|
|
||||||
const html = await renderTemplate('public/index', {
|
try {
|
||||||
title: query ? `Search Results for "${query}" | Privacy Policy Analyzer` : 'Search Services',
|
let services;
|
||||||
description: 'Search for services and see their privacy grades.',
|
|
||||||
canonical: `${req.protocol}://${url.host}/search`,
|
if (query && query.trim().length > 0) {
|
||||||
services
|
// Use Meilisearch for search
|
||||||
});
|
console.log(`Searching for: ${query}`);
|
||||||
|
const searchResults = await SearchIndexer.search(query, { limit: 50 });
|
||||||
return new Response(html, {
|
services = searchResults.hits.map(hit => ({
|
||||||
headers: { 'Content-Type': 'text/html' }
|
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
|
// Admin login page - GET /admin/login
|
||||||
@@ -201,7 +243,7 @@ async function handleRequest(req) {
|
|||||||
|
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.set('Content-Type', 'application/json');
|
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({
|
return new Response(JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -249,6 +291,9 @@ async function handleRequest(req) {
|
|||||||
return new Response(null, { status: 302, headers });
|
return new Response(null, { status: 302, headers });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extend session on each request
|
||||||
|
await AdminSession.extendSession(sessionToken);
|
||||||
|
|
||||||
// Clean up expired sessions
|
// Clean up expired sessions
|
||||||
await AdminSession.deleteExpired();
|
await AdminSession.deleteExpired();
|
||||||
|
|
||||||
@@ -354,9 +399,17 @@ async function handleRequest(req) {
|
|||||||
return new Response('Name, URL, and policy URL are required', { status: 400 });
|
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');
|
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
|
// Invalidate homepage cache
|
||||||
await PageCache.invalidateHomepage();
|
await PageCache.invalidateHomepage();
|
||||||
|
|
||||||
@@ -436,6 +489,15 @@ async function handleRequest(req) {
|
|||||||
await Service.update(id, data);
|
await Service.update(id, data);
|
||||||
console.log('Service updated successfully');
|
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
|
// Invalidate caches
|
||||||
await PageCache.invalidateHomepage();
|
await PageCache.invalidateHomepage();
|
||||||
await PageCache.invalidateService(id);
|
await PageCache.invalidateService(id);
|
||||||
@@ -467,6 +529,14 @@ async function handleRequest(req) {
|
|||||||
const id = parseInt(match[1]);
|
const id = parseInt(match[1]);
|
||||||
await Service.delete(id);
|
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
|
// Invalidate caches
|
||||||
await PageCache.invalidateHomepage();
|
await PageCache.invalidateHomepage();
|
||||||
await PageCache.invalidateService(id);
|
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
|
// Sitemap.xml - GET /sitemap.xml
|
||||||
if (method === 'GET' && pathname === '/sitemap.xml') {
|
if (method === 'GET' && pathname === '/sitemap.xml') {
|
||||||
try {
|
try {
|
||||||
@@ -684,6 +784,15 @@ console.log(`Server running at http://localhost:${server.port}`);
|
|||||||
await SearchIndexer.init();
|
await SearchIndexer.init();
|
||||||
console.log('Search indexer initialized');
|
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
|
// Start analysis worker
|
||||||
AnalysisWorker.start();
|
AnalysisWorker.start();
|
||||||
console.log('Analysis worker started');
|
console.log('Analysis worker started');
|
||||||
|
|||||||
@@ -123,6 +123,27 @@ export class AnalysisWorker {
|
|||||||
|
|
||||||
console.log(`[${jobId}] Analysis complete: Grade ${analysis.overall_score}`);
|
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
|
// Invalidate caches
|
||||||
try {
|
try {
|
||||||
await PageCache.invalidateHomepage();
|
await PageCache.invalidateHomepage();
|
||||||
|
|||||||
Reference in New Issue
Block a user