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
|
||||
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');
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user