Data: 2026-01-10
Versione: 1.0
Soluzione: Backend API con Autenticazione + Upload CSV
Riferimento: data-security-analysis.md - Soluzione 1
Questo documento descrive la progettazione dettagliata della Soluzione 1: Backend API con Autenticazione, estesa con la funzionalità di upload CSV e generazione automatica dei file JSON utilizzati dall'applicazione.
- ✅ Protezione completa dei dati: Nessun file (CSV o JSON) accessibile direttamente dall'esterno
- ✅ Upload CSV sicuro: Solo utenti autenticati possono caricare file CSV
- ✅ Generazione automatica JSON: Il backend processa il CSV e genera il JSON ottimizzato
- ✅ API protette: Autenticazione JWT/API key per tutte le operazioni
- ✅ Rate limiting: Prevenzione scraping e abusi
- ✅ Audit trail: Log completo di upload e accessi
┌─────────────────────────────────────────────────────────────────────┐
│ FLUSSO DATI CON BACKEND API │
└─────────────────────────────────────────────────────────────────────┘
1. UPLOAD CSV (Admin)
Admin → POST /api/admin/upload (CSV + Auth) → Backend
↓
Backend processa CSV → Genera JSON → Salva in directory privata
↓
Notifica success → Admin
2. CONSUMO DATI (Client)
Browser → GET /api/lemmi (Auth) → Backend
↓
Backend legge JSON privato → Ritorna dati
↓
Browser riceve JSON (no download diretto)
3. PROTEZIONE
Browser → GET /data/lemmi.json → Nginx → ❌ 403 Forbidden
Browser → GET /uploads/*.csv → Nginx → ❌ 403 Forbidden
Garanzia Sicurezza:
- ❌ CSV NON scaricabile (directory privata, no endpoint pubblico)
- ❌ JSON NON scaricabile direttamente (servito solo via API con auth)
- ✅ Dati accessibili SOLO tramite API autenticata
- ✅ Upload SOLO da admin autenticati
lemmario-dashboard/
├── server/ # Backend Node.js/Express
│ ├── index.js # Entry point server
│ ├── config/
│ │ └── config.js # Configurazione (env vars)
│ ├── routes/
│ │ ├── data.js # GET /api/lemmi, /api/geojson, /api/regions
│ │ └── admin.js # POST /api/admin/upload, GET /api/admin/status
│ ├── middleware/
│ │ ├── auth.js # Autenticazione JWT/API key
│ │ ├── adminAuth.js # Autenticazione admin (ruolo elevato)
│ │ ├── rateLimit.js # Rate limiting
│ │ ├── upload.js # Multer upload middleware
│ │ └── errorHandler.js # Error handling centralizzato
│ ├── services/
│ │ ├── csvProcessor.js # Processamento CSV → JSON
│ │ ├── dataValidator.js # Validazione dati CSV
│ │ └── auditLogger.js # Logging operazioni
│ ├── data/ # 🔒 DIRECTORY PRIVATA
│ │ ├── lemmi.json # Generato da CSV
│ │ ├── geojson.json # Generato da CSV
│ │ └── limits_IT_regions.geojson # Statico
│ ├── uploads/ # 🔒 CSV caricati (temporanei)
│ │ └── .gitkeep
│ ├── logs/ # Audit logs
│ │ └── audit.log
│ ├── package.json
│ └── README.md
├── public/ # SOLO asset pubblici (no data)
│ ├── logo/
│ └── images/
└── src/ # Frontend Next.js
└── services/
└── dataLoader.ts # Modificato per chiamare API
| Componente | Tecnologia | Versione | Scopo |
|---|---|---|---|
| Runtime | Node.js | 20 LTS | Esecuzione JavaScript server-side |
| Framework | Express.js | ^4.18 | Web server e routing |
| Autenticazione | jsonwebtoken | ^9.0 | JWT token generation/validation |
| Upload | multer | ^1.4 | Gestione upload file multipart |
| CSV Parsing | papaparse | ^5.5 | Parsing CSV (stesso del frontend) |
| Rate Limiting | express-rate-limit | ^7.0 | Prevenzione abusi API |
| CORS | cors | ^2.8 | Cross-Origin Resource Sharing |
| Validazione | joi | ^17.0 | Schema validation |
| Logging | winston | ^3.11 | Logging strutturato |
| Process Manager | pm2 | ^5.3 | Gestione processi produzione |
Permessi:
- ✅ Lettura dati tramite API autenticata (con API key frontend)
- ❌ Upload CSV
- ❌ Modifica dati
- ❌ Accesso diretto a file
Autenticazione: API key embedded nel frontend (rotazione periodica)
Permessi:
- ✅ Upload CSV
- ✅ Visualizzazione stato processamento
- ✅ Download logs (opzionale)
- ✅ Gestione API keys (opzionale)
Autenticazione: JWT token con credenziali username/password
Come amministratore dei dati
Voglio caricare un nuovo file CSV con i dati aggiornati
Così che l'applicazione utilizzi i dati più recenti senza deployment manuale
Acceptance Criteria:
- ✅ Login con credenziali admin → ricevo JWT token
- ✅ Upload CSV via form/API → backend valida formato
- ✅ Backend processa CSV → genera JSON ottimizzato
- ✅ Ricevo conferma processamento con statistiche (record processati, errori)
- ✅ Applicazione frontend utilizza immediatamente nuovi dati (o dopo cache invalidation)
- ✅ CSV originale salvato in directory privata (backup/audit)
Flow:
1. Admin → POST /api/admin/login
Body: { username: "admin", password: "***" }
← Response: { token: "eyJhbG..." }
2. Admin → POST /api/admin/upload
Headers: { Authorization: "Bearer eyJhbG..." }
Body: FormData { file: lemmi.csv }
← Response: {
status: "processing",
jobId: "uuid-123"
}
3. Admin → GET /api/admin/status/:jobId
← Response: {
status: "completed",
records: 15234,
errors: [],
timestamp: "2026-01-10T15:30:00Z"
}
4. Frontend app → GET /api/lemmi
← Response: { /* nuovi dati JSON */ }
Come applicazione frontend
Voglio accedere ai dati tramite API
Così che possa visualizzare lemmi, aree geografiche e timeline
Acceptance Criteria:
- ✅ Chiamo API con API key valida → ricevo dati JSON
- ✅ API restituisce solo dati necessari (no metadati sensibili)
- ✅ Rate limiting previene download massivo
- ✅ Nessun accesso diretto a file CSV o JSON sul filesystem
Flow:
Frontend → GET /api/lemmi
Headers: { X-API-Key: "frontend-key-abc123" }
← Response: [
{
IdLemma: 1,
Lemma: "abbacchio",
Forma: "abbacchio",
CollGeografica: "Roma",
...
},
...
]
Come amministratore
Voglio visualizzare log di upload e accessi
Così che possa monitorare l'utilizzo e identificare anomalie
Acceptance Criteria:
- ✅ Visualizzazione log upload (chi, quando, risultato)
- ✅ Statistiche accessi API (rate, IP sorgente)
- ✅ Alert su upload falliti o accessi anomali
# Directory server/data/ e server/uploads/
chmod 700 server/data server/uploads
chown node:node server/data server/uploads
# File CSV e JSON
chmod 600 server/data/*.json
chmod 600 server/uploads/*.csvRisultato: Solo processo Node.js può leggere/scrivere file
server {
listen 9000;
root /usr/share/nginx/html;
# ❌ BLOCCO TOTALE directory /data
location /data/ {
deny all;
return 403;
}
# ❌ BLOCCO directory /uploads
location /uploads/ {
deny all;
return 403;
}
# ❌ BLOCCO file .csv ovunque
location ~ \.csv$ {
deny all;
return 403;
}
# ✅ PROXY API requests a backend Node.js
location /api/ {
proxy_pass http://backend:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Rate limiting
limit_req zone=api_limit burst=20 nodelay;
limit_req_status 429;
}
# SPA routing
location / {
try_files $uri $uri/ /index.html;
}
}
# Rate limit zone
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;Risultato:
- ✅ Richieste a
/data/lemmi.json→ HTTP 403 - ✅ Richieste a
/uploads/file.csv→ HTTP 403 - ✅ Richieste a
/api/lemmi→ Proxy a backend (con auth)
Per API Dati (Frontend): API Key
// middleware/auth.js
const authMiddleware = (req, res, next) => {
const apiKey = req.headers['x-api-key'];
const validKeys = process.env.FRONTEND_API_KEYS?.split(',') || [];
if (!validKeys.includes(apiKey)) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid or missing API key'
});
}
// Logging accesso
logger.info('API access', {
endpoint: req.path,
ip: req.ip,
apiKey: apiKey.substring(0, 8) + '...'
});
next();
};Per Upload Admin: JWT Token
// middleware/adminAuth.js
const jwt = require('jsonwebtoken');
const adminAuthMiddleware = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer <token>
if (!token) {
return res.status(401).json({ error: 'Missing authentication token' });
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
// Verifica ruolo admin
if (payload.role !== 'admin') {
return res.status(403).json({ error: 'Insufficient permissions' });
}
req.user = payload;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
};// middleware/rateLimit.js
const rateLimit = require('express-rate-limit');
// Rate limit per API dati (frontend)
const dataApiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minuti
max: 100, // 100 richieste per finestra
message: 'Too many requests from this IP, please try again later.',
standardHeaders: true,
legacyHeaders: false,
});
// Rate limit per upload admin (più restrittivo)
const uploadLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 ora
max: 5, // Massimo 5 upload per ora
message: 'Upload limit exceeded. Please wait before uploading again.',
skipSuccessfulRequests: false,
});
module.exports = { dataApiLimiter, uploadLimiter };# Redirect HTTP → HTTPS
server {
listen 80;
server_name atliteg-map.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name atliteg-map.example.com;
ssl_certificate /etc/letsencrypt/live/atliteg-map.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/atliteg-map.example.com/privkey.pem;
# ... resto configurazione
}| Asset | Accesso Diretto | Via API Pubblica | Via API Admin | Protezione |
|---|---|---|---|---|
| CSV originale | ❌ No | ❌ No | ❌ No | Filesystem + Nginx |
| JSON generato | ❌ No | ❌ No | ❌ No | Filesystem + Nginx |
| Dati via API | N/A | ✅ Sì (auth) | ✅ Sì (auth) | API Key + Rate Limit |
| Upload CSV | ❌ No | ❌ No | ✅ Sì (auth) | JWT + Upload Limit |
| Logs | ❌ No | ❌ No | ✅ Sì (auth) | JWT + Role Check |
Attacco: GET https://atliteg-map.com/data/lemmi.json
Mitigazione:
- Nginx blocca request → HTTP 403
- Log tentativo accesso (monitoring)
- File fisicamente non accessibile da web root
Risultato: ❌ Attacco fallito
Attacco: Script automatico chiama GET /api/lemmi ripetutamente
Mitigazione:
- Rate limiting: max 100 req/15min per IP
- Dopo 100 richieste → HTTP 429 Too Many Requests
- Possibile ban temporaneo IP (opzionale)
- Monitoring anomalie (spike richieste)
Risultato:
Attacco: API key frontend esposta in codice sorgente
Mitigazione:
- API key rotazione periodica (es. mensile)
- Rate limiting per key
- Monitoring utilizzo per key
- Possibilità disabilitare key compromessa
- Nota: Protezione completa richiede auth utente (OAuth, etc.)
Risultato:
Attacco: Admin account compromesso, upload CSV con dati malevoli
Mitigazione:
- Validazione rigorosa CSV (schema, tipi dati, dimensione)
- Sanitizzazione input (rimozione caratteri speciali)
- Quarantena file: validazione prima di processare
- Rollback automatico su errori
- Audit completo: chi ha caricato cosa e quando
- Backup automatico file precedente
Risultato: ✅ Attacco mitigato con validazione e audit
Scopo: Autenticazione admin e generazione JWT token
Request:
POST /api/admin/login
Content-Type: application/json
{
"username": "admin",
"password": "secure_password_here"
}Response Success:
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": "24h",
"user": {
"username": "admin",
"role": "admin"
}
}Response Error:
{
"success": false,
"error": "Invalid credentials"
}Implementazione:
// routes/admin.js
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const router = express.Router();
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
// Validazione input
if (!username || !password) {
return res.status(400).json({
success: false,
error: 'Username and password required'
});
}
// Verifica credenziali (esempio con env vars)
const adminUsername = process.env.ADMIN_USERNAME;
const adminPasswordHash = process.env.ADMIN_PASSWORD_HASH;
if (username !== adminUsername) {
return res.status(401).json({
success: false,
error: 'Invalid credentials'
});
}
const validPassword = await bcrypt.compare(password, adminPasswordHash);
if (!validPassword) {
return res.status(401).json({
success: false,
error: 'Invalid credentials'
});
}
// Genera JWT
const token = jwt.sign(
{
username,
role: 'admin',
iat: Math.floor(Date.now() / 1000)
},
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
// Audit log
logger.info('Admin login successful', { username, ip: req.ip });
res.json({
success: true,
token,
expiresIn: '24h',
user: { username, role: 'admin' }
});
} catch (error) {
logger.error('Login error', { error: error.message });
res.status(500).json({
success: false,
error: 'Internal server error'
});
}
});
module.exports = router;Scopo: Upload file CSV e trigger processamento
Request:
POST /api/admin/upload
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: multipart/form-data
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="lemmi_updated.csv"
Content-Type: text/csv
IdLemma,Lemma,Forma,Coll.Geografica,...
1,abbacchio,abbacchio,Roma,...
...Response Success (Async Processing):
{
"success": true,
"message": "Upload successful, processing started",
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"statusUrl": "/api/admin/status/550e8400-e29b-41d4-a716-446655440000"
}Response Error:
{
"success": false,
"error": "Invalid file format",
"details": "Expected CSV, got application/octet-stream"
}Implementazione:
// routes/admin.js
const multer = require('multer');
const { v4: uuidv4 } = require('uuid');
const path = require('path');
const csvProcessor = require('../services/csvProcessor');
const adminAuthMiddleware = require('../middleware/adminAuth');
const { uploadLimiter } = require('../middleware/rateLimit');
// Configurazione upload
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, path.join(__dirname, '../uploads'));
},
filename: (req, file, cb) => {
const timestamp = Date.now();
const safeFilename = file.originalname.replace(/[^a-zA-Z0-9._-]/g, '_');
cb(null, `${timestamp}_${safeFilename}`);
}
});
const upload = multer({
storage,
limits: {
fileSize: 10 * 1024 * 1024, // 10 MB max
files: 1
},
fileFilter: (req, file, cb) => {
// Accetta solo CSV
if (file.mimetype === 'text/csv' ||
file.mimetype === 'application/csv' ||
file.originalname.endsWith('.csv')) {
cb(null, true);
} else {
cb(new Error('Only CSV files are allowed'));
}
}
});
router.post('/upload',
adminAuthMiddleware,
uploadLimiter,
upload.single('file'),
async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
error: 'No file uploaded'
});
}
const jobId = uuidv4();
const uploadInfo = {
jobId,
filename: req.file.filename,
originalName: req.file.originalname,
size: req.file.size,
uploadedBy: req.user.username,
uploadedAt: new Date().toISOString(),
status: 'processing'
};
// Audit log
logger.info('CSV upload received', uploadInfo);
// Avvia processamento asincrono
csvProcessor.processCSV(req.file.path, jobId)
.then(result => {
logger.info('CSV processing completed', {
jobId,
records: result.recordCount
});
})
.catch(error => {
logger.error('CSV processing failed', {
jobId,
error: error.message
});
});
res.json({
success: true,
message: 'Upload successful, processing started',
jobId,
statusUrl: `/api/admin/status/${jobId}`
});
} catch (error) {
logger.error('Upload error', { error: error.message });
res.status(500).json({
success: false,
error: error.message || 'Upload failed'
});
}
}
);Scopo: Verifica stato processamento CSV
Request:
GET /api/admin/status/550e8400-e29b-41d4-a716-446655440000
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Response:
{
"success": true,
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"progress": {
"recordsProcessed": 15234,
"recordsTotal": 15234,
"errors": [],
"warnings": [
"Line 1023: Missing IdAmbito, skipped"
]
},
"timing": {
"startedAt": "2026-01-10T15:30:00Z",
"completedAt": "2026-01-10T15:30:45Z",
"duration": "45s"
},
"output": {
"lemmiJson": "/data/lemmi.json",
"geojsonJson": "/data/geojson.json",
"recordCount": 15234
}
}Scopo: Recupero dati lemmi (frontend)
Request:
GET /api/lemmi
X-API-Key: frontend_key_abc123xyzResponse:
[
{
"IdLemma": 1,
"Lemma": "abbacchio",
"Forma": "abbacchio",
"CollGeografica": "Roma",
"Latitudine": "41.9028",
"Longitudine": "12.4964",
"Anno": "1250",
"Periodo": "Sec. XIII",
"Categoria": "Carne",
"Frequenza": 45,
"URL": "https://vosl.it/...",
"IdAmbito": "5"
},
...
]Rate Limiting: 100 req/15min per IP
Implementazione:
// routes/data.js
const express = require('express');
const fs = require('fs').promises;
const path = require('path');
const authMiddleware = require('../middleware/auth');
const { dataApiLimiter } = require('../middleware/rateLimit');
const router = express.Router();
router.get('/lemmi',
authMiddleware,
dataApiLimiter,
async (req, res) => {
try {
const dataPath = path.join(__dirname, '../data/lemmi.json');
const data = await fs.readFile(dataPath, 'utf-8');
// Parse e ritorna
const lemmi = JSON.parse(data);
// Log accesso (opzionale, commentabile per performance)
// logger.debug('Lemmi data accessed', { ip: req.ip });
res.json(lemmi);
} catch (error) {
logger.error('Error loading lemmi data', { error: error.message });
res.status(500).json({
error: 'Failed to load data',
message: 'Internal server error'
});
}
}
);
module.exports = router;File: server/services/csvProcessor.js
const Papa = require('papaparse');
const fs = require('fs').promises;
const path = require('path');
const logger = require('./logger');
class CSVProcessor {
constructor() {
this.jobs = new Map(); // In-memory job tracking (può essere Redis)
}
async processCSV(csvFilePath, jobId) {
const startTime = Date.now();
try {
// Update job status
this.jobs.set(jobId, {
status: 'processing',
startedAt: new Date().toISOString()
});
// Leggi CSV
const csvContent = await fs.readFile(csvFilePath, 'utf-8');
// Parse CSV (same logic as preprocessing script)
const result = await new Promise((resolve, reject) => {
Papa.parse(csvContent, {
header: true,
skipEmptyLines: true,
transformHeader: (header) => {
const headerMap = {
'IdLemma': 'IdLemma',
'Lemma': 'Lemma',
'Forma': 'Forma',
'Coll.Geografica': 'CollGeografica',
'Latitudine': 'Latitudine',
'Longitudine': 'Longitudine',
'Tipo coll.Geografica': 'TipoCollGeografica',
'Anno': 'Anno',
'Periodo': 'Periodo',
'IDPeriodo': 'IDPeriodo',
'Datazione': 'Datazione',
'Categoria': 'Categoria',
'Frequenza': 'Frequenza',
'URL': 'URL',
'IdAmbito': 'IdAmbito',
'reg_istat_code': 'RegionIstatCode',
};
return headerMap[header] || header;
},
complete: (results) => resolve(results),
error: (error) => reject(error)
});
});
const lemmi = result.data;
const warnings = [];
// Validazione base
if (lemmi.length === 0) {
throw new Error('CSV file is empty');
}
// Salva JSON ottimizzato
const outputPath = path.join(__dirname, '../data/lemmi.json');
await fs.writeFile(outputPath, JSON.stringify(lemmi, null, 0));
// Backup CSV originale (opzionale)
const backupPath = path.join(
__dirname,
'../uploads/backup',
`${jobId}_${path.basename(csvFilePath)}`
);
await fs.mkdir(path.dirname(backupPath), { recursive: true });
await fs.copyFile(csvFilePath, backupPath);
// Rimuovi CSV temporaneo
await fs.unlink(csvFilePath);
const endTime = Date.now();
const duration = Math.round((endTime - startTime) / 1000);
// Update job status
const jobResult = {
status: 'completed',
startedAt: this.jobs.get(jobId).startedAt,
completedAt: new Date().toISOString(),
duration: `${duration}s`,
recordCount: lemmi.length,
warnings,
output: {
lemmiJson: outputPath,
recordCount: lemmi.length
}
};
this.jobs.set(jobId, jobResult);
logger.info('CSV processing completed', { jobId, ...jobResult });
return jobResult;
} catch (error) {
// Update job status
this.jobs.set(jobId, {
status: 'failed',
error: error.message,
startedAt: this.jobs.get(jobId)?.startedAt,
failedAt: new Date().toISOString()
});
logger.error('CSV processing failed', { jobId, error: error.message });
throw error;
}
}
getJobStatus(jobId) {
return this.jobs.get(jobId) || { status: 'not_found' };
}
}
module.exports = new CSVProcessor();File: lemmario-dashboard/Dockerfile.backend
# Multi-stage build per Backend + Frontend
FROM node:20-alpine AS backend-builder
WORKDIR /app/server
# Install backend dependencies
COPY server/package*.json ./
RUN npm ci --production
# Copy backend code
COPY server/ ./
# ---
FROM node:20-alpine AS frontend-builder
WORKDIR /app
# Install frontend dependencies
COPY lemmario-dashboard/package*.json ./
RUN npm ci
# Copy frontend code and build
COPY lemmario-dashboard/ ./
RUN npm run build
# ---
FROM node:20-alpine AS production
WORKDIR /app
# Install pm2 globally
RUN npm install -g pm2
# Copy backend
COPY --from=backend-builder /app/server ./server
# Copy frontend static build
COPY --from=frontend-builder /app/out ./public
# Create data directories
RUN mkdir -p /app/server/data /app/server/uploads /app/server/logs && \
chown -R node:node /app
# Copy initial data files (se esistono)
COPY data/*.json /app/server/data/ || true
COPY data/*.geojson /app/server/data/ || true
USER node
EXPOSE 3001
# Start backend with pm2
CMD ["pm2-runtime", "start", "server/index.js", "--name", "atliteg-api"]File: docker-compose.yml (updated)
version: '3.8'
services:
backend:
build:
context: .
dockerfile: lemmario-dashboard/Dockerfile.backend
container_name: atliteg-backend
ports:
- "3001:3001"
environment:
- NODE_ENV=production
- PORT=3001
- FRONTEND_API_KEYS=${FRONTEND_API_KEYS}
- ADMIN_USERNAME=${ADMIN_USERNAME}
- ADMIN_PASSWORD_HASH=${ADMIN_PASSWORD_HASH}
- JWT_SECRET=${JWT_SECRET}
- ALLOWED_ORIGINS=https://atliteg-map.example.com
volumes:
- ./server/data:/app/server/data
- ./server/uploads:/app/server/uploads
- ./server/logs:/app/server/logs
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3001/health"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
nginx:
image: nginx:alpine
container_name: atliteg-nginx
ports:
- "9000:9000"
volumes:
- ./lemmario-dashboard/out:/usr/share/nginx/html:ro
- ./lemmario-dashboard/nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- backend
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:9000/health"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stoppedFile: .env.example
# Backend API Configuration
FRONTEND_API_KEYS=frontend_key_abc123xyz,frontend_key_backup_456
# Admin Credentials
ADMIN_USERNAME=admin
# Generate hash: echo -n "your_password" | bcrypt
ADMIN_PASSWORD_HASH=$2b$10$abcdefghijklmnopqrstuv...
# JWT Secret (generate with: openssl rand -hex 32)
JWT_SECRET=your_jwt_secret_here_min_32_chars
# CORS
ALLOWED_ORIGINS=https://atliteg-map.example.com,http://localhost:3000# 1. Clone repository
git clone https://github.com/Unica-dh/atliteg-map.git
cd atliteg-map
# 2. Crea file .env
cp .env.example .env
nano .env # Configura credenziali
# 3. Genera password hash
npm install -g bcrypt-cli
echo -n "my_admin_password" | bcrypt
# 4. Build e avvia
docker-compose up --build -d
# 5. Verifica salute servizi
docker-compose ps
curl http://localhost:3001/health # Backend
curl http://localhost:9000/health # Nginx# 1. Login admin
curl -X POST http://localhost:3001/api/admin/login \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "my_admin_password"
}' \
| jq -r '.token' > token.txt
TOKEN=$(cat token.txt)
# 2. Upload CSV
curl -X POST http://localhost:3001/api/admin/upload \
-H "Authorization: Bearer $TOKEN" \
-F "file=@data/Lemmi_forme_atliteg_updated.csv" \
| jq '.'
# Output:
# {
# "success": true,
# "jobId": "550e8400-e29b-41d4-a716-446655440000",
# "statusUrl": "/api/admin/status/550e8400-e29b-41d4-a716-446655440000"
# }
# 3. Verifica stato
JOB_ID="550e8400-e29b-41d4-a716-446655440000"
curl -X GET "http://localhost:3001/api/admin/status/$JOB_ID" \
-H "Authorization: Bearer $TOKEN" \
| jq '.'// lemmario-dashboard/src/services/dataLoader.ts
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
const API_KEY = process.env.NEXT_PUBLIC_API_KEY;
export async function loadCSVData(): Promise<Lemma[]> {
try {
const response = await fetch(`${API_BASE_URL}/api/lemmi`, {
headers: {
'X-API-Key': API_KEY || ''
}
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
console.log(`✅ Dati caricati: ${data.length} record`);
return data;
} catch (error) {
console.error('❌ Errore caricamento dati:', error);
throw error;
}
}# Test 1: Verifica blocco accesso diretto CSV
curl -I http://localhost:9000/data/Lemmi_forme_atliteg.csv
# Atteso: HTTP/1.1 403 Forbidden
# Test 2: Verifica blocco accesso diretto JSON
curl -I http://localhost:9000/data/lemmi.json
# Atteso: HTTP/1.1 403 Forbidden
# Test 3: Verifica API senza autenticazione
curl http://localhost:3001/api/lemmi
# Atteso: HTTP/1.1 401 Unauthorized
# Test 4: Verifica API con chiave invalida
curl -H "X-API-Key: invalid_key" http://localhost:3001/api/lemmi
# Atteso: HTTP/1.1 401 Unauthorized
# Test 5: Verifica API con chiave valida
curl -H "X-API-Key: frontend_key_abc123xyz" http://localhost:3001/api/lemmi
# Atteso: HTTP/1.1 200 OK + JSON data
# Test 6: Verifica rate limiting
for i in {1..120}; do
curl -H "X-API-Key: frontend_key_abc123xyz" \
http://localhost:3001/api/lemmi > /dev/null 2>&1
done
# Dopo 100 richieste: HTTP/1.1 429 Too Many Requests
# Test 7: Upload senza autenticazione
curl -X POST http://localhost:3001/api/admin/upload \
-F "file=@test.csv"
# Atteso: HTTP/1.1 401 Unauthorized
# Test 8: Upload con token scaduto
curl -X POST http://localhost:3001/api/admin/upload \
-H "Authorization: Bearer expired_token" \
-F "file=@test.csv"
# Atteso: HTTP/1.1 401 Unauthorized# Test 9: Upload CSV valido
TOKEN="valid_jwt_token_here"
curl -X POST http://localhost:3001/api/admin/upload \
-H "Authorization: Bearer $TOKEN" \
-F "file=@data/Lemmi_forme_atliteg_updated.csv" \
| jq '.'
# Atteso: { "success": true, "jobId": "..." }
# Test 10: Verifica generazione JSON
docker exec atliteg-backend ls -lh /app/server/data/
# Atteso: lemmi.json aggiornato con timestamp recente
# Test 11: Verifica contenuto JSON
docker exec atliteg-backend head -n 20 /app/server/data/lemmi.json
# Atteso: Array JSON con record lemmiWinston Logger Configuration:
// server/services/logger.js
const winston = require('winston');
const path = require('path');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
// Console log
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}),
// File log - errori
new winston.transports.File({
filename: path.join(__dirname, '../logs/error.log'),
level: 'error',
maxsize: 10485760, // 10MB
maxFiles: 5
}),
// File log - audit
new winston.transports.File({
filename: path.join(__dirname, '../logs/audit.log'),
level: 'info',
maxsize: 10485760,
maxFiles: 10
})
]
});
module.exports = logger;Audit Log Format:
{
"timestamp": "2026-01-10T15:30:00.123Z",
"level": "info",
"message": "CSV upload received",
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"filename": "1736524200_lemmi_updated.csv",
"originalName": "Lemmi_forme_atliteg_updated.csv",
"size": 1180659,
"uploadedBy": "admin",
"ip": "192.168.1.100"
}Endpoint: GET /health
// server/index.js
app.get('/health', (req, res) => {
const healthcheck = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
checks: {
dataFiles: 'checking...'
}
};
// Verifica esistenza file dati
const dataPath = path.join(__dirname, 'data/lemmi.json');
fs.access(dataPath, fs.constants.R_OK)
.then(() => {
healthcheck.checks.dataFiles = 'ok';
res.status(200).json(healthcheck);
})
.catch(() => {
healthcheck.checks.dataFiles = 'error';
healthcheck.status = 'degraded';
res.status(503).json(healthcheck);
});
});#!/bin/bash
# backup-data.sh
BACKUP_DIR="/backups/atliteg-data"
DATE=$(date +%Y%m%d_%H%M%S)
# Crea directory backup
mkdir -p "$BACKUP_DIR"
# Backup file JSON
docker cp atliteg-backend:/app/server/data/lemmi.json \
"$BACKUP_DIR/lemmi_$DATE.json"
docker cp atliteg-backend:/app/server/data/geojson.json \
"$BACKUP_DIR/geojson_$DATE.json"
# Backup CSV uploads
docker cp atliteg-backend:/app/server/uploads/backup \
"$BACKUP_DIR/csv_$DATE"
# Backup logs
docker cp atliteg-backend:/app/server/logs \
"$BACKUP_DIR/logs_$DATE"
# Comprimi
tar -czf "$BACKUP_DIR/atliteg_backup_$DATE.tar.gz" \
"$BACKUP_DIR/*_$DATE.*"
# Rimuovi file non compressi
rm -rf "$BACKUP_DIR/*_$DATE.json" \
"$BACKUP_DIR/csv_$DATE" \
"$BACKUP_DIR/logs_$DATE"
# Mantieni solo ultimi 30 giorni
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +30 -delete
echo "✅ Backup completato: atliteg_backup_$DATE.tar.gz"Cron job (esegui backup giornaliero):
# crontab -e
0 2 * * * /opt/atliteg-map/backup-data.sh >> /var/log/atliteg-backup.log 2>&1Giorno 1: Setup Express Server
- Inizializzare progetto Node.js in
server/ - Installare dipendenze (express, cors, multer, etc.)
- Creare struttura directory
- Configurare logging (winston)
- Implementare health check endpoint
- Test locale server
Giorno 2: Autenticazione e Upload
- Implementare JWT authentication
- Creare endpoint
/api/admin/login - Configurare multer per upload
- Implementare endpoint
/api/admin/upload - Creare servizio
csvProcessor.js - Test upload end-to-end
Giorno 3: API Dati e Rate Limiting
- Implementare endpoint
/api/lemmi - Implementare endpoint
/api/geojson - Implementare endpoint
/api/regions - Configurare rate limiting
- Implementare API key authentication
- Test API con Postman/curl
Giorno 4: Modifica Frontend
- Modificare
services/dataLoader.ts - Modificare
hooks/useRegions.ts - Aggiungere variabili ambiente (
.env.local) - Configurare CORS nel backend
- Test integrazione locale
- Verificare funzionalità dashboard
Giorno 5: Containerizzazione
- Creare
Dockerfile.backend - Aggiornare
docker-compose.yml - Aggiornare
nginx.conf(proxy API) - Creare
.env.example - Build Docker images
- Test deploy locale
- Documentazione deployment
Giorno 6: Security & Testing
- Penetration testing (OWASP ZAP)
- Load testing (Apache Bench)
- Verifica HTTPS configuration
- Implementare backup automatico
- Setup monitoring (Prometheus/Grafana opzionale)
- Documentazione operativa
A:
# 1. Genera nuova key
NEW_KEY=$(openssl rand -hex 16)
echo "New API Key: $NEW_KEY"
# 2. Aggiungi alle variabili ambiente (mantieni vecchia)
FRONTEND_API_KEYS=frontend_key_abc123xyz,${NEW_KEY}
# 3. Riavvia backend
docker-compose restart backend
# 4. Aggiorna frontend con nuova key
# .env.local
NEXT_PUBLIC_API_KEY=${NEW_KEY}
# 5. Rebuild frontend
npm run build
# 6. Dopo conferma funzionamento, rimuovi vecchia key
FRONTEND_API_KEYS=${NEW_KEY}
docker-compose restart backendA:
- File temporaneo rimane in
/uploads(manuale cleanup o cron job) - JSON esistente NON viene sovrascritto
- Job status rimane "failed" con dettagli errore
- Admin può ri-tentare upload
- Implementare rollback automatico in
csvProcessor.js
A:
- Aumentare
maxFileSizein multer config - Implementare streaming parse (no caricamento intero in RAM)
- Considerare chunked upload (multipart)
- Aggiungere progress reporting (WebSocket/Server-Sent Events)
- Possibile timeout Nginx → aumentare
proxy_read_timeout
A: Sì, tecnicamente possibile ma:
- Rate limiting rallenta (100 req/15min)
- IP ban possibile dopo soglia
- Monitoring rileva pattern anomali
- Protezione completa richiederebbe: OAuth user login, quotas per-utente, CAPTCHA
Per applicazione pubblica/accademica: Rischio accettabile, dati eventualmente condivisibili.
A:
# 1. Lista backup disponibili
ls -lh /backups/atliteg-data/
# 2. Estrai backup
tar -xzf /backups/atliteg-data/atliteg_backup_20260109_020000.tar.gz
# 3. Copia file nel container
docker cp lemmi_20260109_020000.json atliteg-backend:/app/server/data/lemmi.json
# 4. Verifica
curl -H "X-API-Key: frontend_key_abc123xyz" http://localhost:3001/api/lemmi | jq '.[0]'
# 5. Riavvia backend (opzionale, per invalidare cache)
docker-compose restart backend- ✅ Backend API completo con autenticazione e upload
- ✅ Frontend integrato che consuma API protette
- ✅ Docker deployment production-ready
- ✅ Documentazione completa per sviluppatori e operatori
- ✅ Script di backup e manutenzione
- ✅ Test suite per validazione sicurezza
| Livello | Protezione | Implementato |
|---|---|---|
| Filesystem | Permissions 600/700 | ✅ |
| Nginx | Blocco /data/, /uploads/ |
✅ |
| API | JWT + API Key | ✅ |
| Rate Limit | 100 req/15min | ✅ |
| Upload | Admin-only, 5 upload/ora | ✅ |
| Audit | Winston logging | ✅ |
| HTTPS | SSL/TLS (produzione) |
- ⏱️ Tempo implementazione: 5 giorni (stimato)
- 💰 Costo operativo aggiuntivo: +20-30% risorse server
- 🔒 CSV accessibili dall'esterno: ❌ NO
- 🔒 JSON accessibili direttamente: ❌ NO
- ✅ Dati accessibili via API: Solo con autenticazione
- ✅ Upload CSV: Solo admin autenticati
- 📊 Audit completo: Sì (chi, cosa, quando)
- Implementare backup automatico con retention policy
- Monitoring avanzato con Prometheus + Grafana (opzionale)
- OAuth2 login per admin (invece di username/password)
- Multi-tenancy se necessario gestire più dataset
- Versioning dati per storico modifiche
- API pagination per dataset molto grandi
- GraphQL endpoint (alternativa a REST, opzionale)
Server completo implementato in lemmario-dashboard/server/:
- Entry Point:
index.js- Server principale con health checks, CORS, logging - Configuration:
config/config.js- Configurazione centralizzata con variabili ambiente - Routes implementate:
/api/lemmi- Recupero dati lemmi (protetto con API key)/api/geojson- Recupero dati GeoJSON (protetto con API key)/api/regions- Recupero regioni italiane (protetto con API key)/api/admin/login- Autenticazione admin (genera JWT)/api/admin/upload- Upload CSV (protetto con JWT)/api/admin/status/:jobId- Verifica stato processamento (protetto con JWT)/health- Health check endpoint
- API Key Auth: Frontend usa header
X-API-Key(configurabile in.env) - JWT Auth: Endpoint admin richiedono Bearer token
- Rate Limiting:
- Data API: 100 richieste / 15 minuti
- Upload API: 5 upload / ora
- Winston Logging: Tutte le operazioni registrate in
server/logs/ - bcrypt: Hashing password per credenziali admin
- csvProcessor.js: Gestisce upload CSV → generazione JSON
- Processamento Asincrono: Upload ritorna jobId immediatamente, processamento in background
- Backup: CSV originale salvato in
server/uploads/backup/ - Validazione: Validazione base (controllo file vuoto)
- Tracking Stato: Job tracking in-memory con status/progress
Frontend aggiornato per usare backend API invece di file statici:
- dataLoader.ts: Chiama
/api/lemmie/api/geojson - useRegions.ts: Chiama
/api/regions - Variabili Ambiente: Aggiunti
NEXT_PUBLIC_API_URLeNEXT_PUBLIC_API_KEY - Rimossi Fallback: Nessun accesso diretto ai file, solo chiamate API
- Setup Multi-container:
backend+lemmario-dashboard(nginx) - Backend Dockerfile:
server/Dockerfile- Container Node.js - Frontend Dockerfile: Aggiornato per accettare build args per API URL/key
- Nginx Proxy:
nginx.confaggiornato per:- Bloccare completamente directory
/data/(403 Forbidden) - Bloccare file
.csvovunque - Proxy richieste
/api/al backend:3001
- Bloccare completamente directory
- docker-compose.yml: Orchestrazione entrambi i servizi con health checks
- Blocchi Nginx:
- ❌
/data/*→ 403 Forbidden - ❌
*.csv→ 403 Forbidden - ✅
/api/*→ Proxy a backend (con autenticazione)
- ❌
- Dati Server: File iniziali copiati in
server/data/(directory privata) - Nessun Accesso Pubblico: File CSV e JSON non accessibili agli utenti finali
# Health check
curl http://localhost:3001/health
# ✅ Ritorna: {"status":"ok", ...}
# API Lemmi senza chiave
curl http://localhost:3001/api/lemmi
# ✅ Ritorna: {"error":"Unauthorized", "message":"Missing API key"}
# API Lemmi con chiave valida
curl -H "X-API-Key: default_dev_key" http://localhost:3001/api/lemmi
# ✅ Ritorna: 6236 record lemmi
# Login admin
curl -X POST http://localhost:3001/api/admin/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin"}'
# ✅ Ritorna: Token JWT
# Upload CSV
curl -X POST http://localhost:3001/api/admin/upload \
-H "Authorization: Bearer <token>" \
-F "file=@test.csv"
# ✅ Ritorna: {"success":true, "jobId":"...", ...}
# Stato Job
curl http://localhost:3001/api/admin/status/<jobId> \
-H "Authorization: Bearer <token>"
# ✅ Ritorna: {"status":"completed", "recordCount":1, ...}# Backend API
FRONTEND_API_KEYS=default_dev_key
NEXT_PUBLIC_API_KEY=default_dev_key
# Admin (password default: "admin")
ADMIN_USERNAME=admin
ADMIN_PASSWORD_HASH=$2b$10$wqM4/4h7tknyFoihM8wLCuLTv9Ndbs3V1rQ70hsSQtOwa2k47wnQW
# JWT
JWT_SECRET=your_jwt_secret_change_in_production_min_32_chars
# CORS
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:9000
# API URL
NEXT_PUBLIC_API_URL=http://backend:3001Per deployment in produzione, aggiornare .env con:
- API Keys forti: Generare con
openssl rand -hex 16 - JWT Secret forte: Generare con
openssl rand -hex 32 - Password Admin forte:
node -e "require('bcrypt').hash('YOUR_PASSWORD', 10).then(h => console.log(h))" - Domini effettivi per ALLOWED_ORIGINS
lemmario-dashboard/
├── server/ # Backend API
│ ├── index.js # Server entry point
│ ├── config/config.js # Configurazione
│ ├── routes/
│ │ ├── data.js # Endpoint dati API
│ │ └── admin.js # Endpoint admin
│ ├── middleware/
│ │ ├── auth.js # Autenticazione API key
│ │ ├── adminAuth.js # Autenticazione JWT
│ │ ├── rateLimit.js # Rate limiting
│ │ └── errorHandler.js # Gestione errori
│ ├── services/
│ │ ├── csvProcessor.js # Processamento CSV
│ │ └── logger.js # Winston logger
│ ├── data/ # 🔒 Dati privati (non accessibili da web)
│ │ ├── lemmi.json
│ │ ├── geojson.json
│ │ └── limits_IT_regions.geojson
│ ├── uploads/ # 🔒 Upload CSV
│ │ └── backup/ # Backup CSV
│ ├── logs/ # 🔒 Log audit
│ ├── package.json
│ └── Dockerfile
├── services/dataLoader.ts # Modificato per usare API
├── hooks/useRegions.ts # Modificato per usare API
├── nginx.conf # Aggiornato con blocchi + proxy
├── Dockerfile # Aggiornato con build args
├── .env.local # Variabili ambiente locali
└── .env.example # File esempio ambiente
# Build container
cd /home/runner/work/atliteg-map/atliteg-map
docker compose build
# Avvio container
docker compose up -d
# Verifica servizi
docker compose ps
curl http://localhost:9000/health # Nginx
curl http://localhost:3001/health # Backend# Test blocco CSV
curl -I http://localhost:9000/data/Lemmi_forme_atliteg_updated.csv
# Atteso: HTTP/1.1 403 Forbidden
# Test blocco JSON
curl -I http://localhost:9000/data/lemmi.json
# Atteso: HTTP/1.1 403 Forbidden
# Test API tramite nginx
curl -H "X-API-Key: default_dev_key" http://localhost:9000/api/lemmi | jq '. | length'
# Atteso: 6236- Aprire browser: http://localhost:9000
- Verificare caricamento mappa con tutti i marker lemmi
- Verificare funzionamento filtri
- Verificare funzionalità ricerca
- Verificare assenza errori nella console browser
- Catturare screenshot
- Login come admin per ottenere JWT token
- Upload nuovo file CSV
- Verificare stato job
- Verificare nuovi dati nell'applicazione
- Verificare creazione backup in
server/uploads/backup/
- Build Docker Frontend: Build in corso quando interrotto il lavoro
- Test Frontend: Necessario verificare caricamento corretto applicazione con dati API
- Configurazione Produzione: Necessario impostare password e chiavi forti per produzione
- SSL/HTTPS: Non configurato (nginx.conf ha placeholder per HTTPS produzione)
- Persistenza Job: Job memorizzati in-memory (persi al restart). Considerare Redis per produzione
- UI Admin: Nessuna interfaccia admin creata (usare curl/Postman per ora)
const response = await fetch('http://localhost:9000/api/lemmi', {
headers: { 'X-API-Key': 'your_api_key' }
});
const lemmi = await response.json();// 1. Login
const loginRes = await fetch('http://localhost:9000/api/admin/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'admin' })
});
const { token } = await loginRes.json();
// 2. Upload
const formData = new FormData();
formData.append('file', csvFile);
const uploadRes = await fetch('http://localhost:9000/api/admin/upload', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: formData
});
const { jobId } = await uploadRes.json();
// 3. Verifica stato
const statusRes = await fetch(`http://localhost:9000/api/admin/status/${jobId}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const status = await statusRes.json();- Design API Backend: Questo documento (sezioni 1-11)
- Issue Originale: [problem statement in PR description]
- File Esempio Ambiente:
.env.example
Prima di procedere all'implementazione, confermare:
-
✅ Autenticazione admin: Username/password + JWT è sufficiente, o serve integrazione LDAP/OAuth?
-
✅ Rotazione API keys: Rotazione manuale periodica è accettabile, o serve automazione?
-
✅ Gestione file GeoJSON: Anche GeoJSON (
Ambiti geolinguistici newline.json) deve essere generato da CSV, o può rimanere statico? -
✅ Validazione CSV: Quali validazioni specifiche sul CSV? (es. campi obbligatori, formati, range valori)
-
✅ Notifiche upload: Email/Slack notification quando upload completato?
-
✅ Interfaccia admin: Serve UI web per upload, o CLI/API è sufficiente?
-
✅ Backup/Rollback: Backup automatico giornaliero è sufficiente? Serve rollback UI?
-
✅ Performance: Quanti utenti concorrenti previsti? (per dimensionare rate limits)
-
✅ Compliance: Requisiti GDPR/privacy specifici oltre quelli standard?
-
✅ Budget deployment: Hosting self-managed o cloud managed (AWS/Azure)?
Documento pronto per revisione e approvazione
Prossimo step: Feedback su requisiti → Implementazione Fase 1