Skip to content

Latest commit

 

History

History
1861 lines (1486 loc) · 50.9 KB

File metadata and controls

1861 lines (1486 loc) · 50.9 KB

Backend API con Upload CSV - Analisi e Progettazione

Documento di Progettazione Dettagliata

Data: 2026-01-10
Versione: 1.0
Soluzione: Backend API con Autenticazione + Upload CSV
Riferimento: data-security-analysis.md - Soluzione 1


1. Executive Summary

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.

Requisiti Chiave

  1. Protezione completa dei dati: Nessun file (CSV o JSON) accessibile direttamente dall'esterno
  2. Upload CSV sicuro: Solo utenti autenticati possono caricare file CSV
  3. Generazione automatica JSON: Il backend processa il CSV e genera il JSON ottimizzato
  4. API protette: Autenticazione JWT/API key per tutte le operazioni
  5. Rate limiting: Prevenzione scraping e abusi
  6. Audit trail: Log completo di upload e accessi

Flusso Dati Proposto

┌─────────────────────────────────────────────────────────────────────┐
│                      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

2. Architettura Componente Backend

2.1 Struttura Directory

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

2.2 Stack Tecnologico Backend

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

3. Funzionalità Utente

3.1 Ruoli Utente

Ruolo: Public User (Frontend Application)

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)

Ruolo: Admin (Data Manager)

Permessi:

  • ✅ Upload CSV
  • ✅ Visualizzazione stato processamento
  • ✅ Download logs (opzionale)
  • ✅ Gestione API keys (opzionale)

Autenticazione: JWT token con credenziali username/password

3.2 User Stories

US-1: Upload CSV come Admin

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:

  1. ✅ Login con credenziali admin → ricevo JWT token
  2. ✅ Upload CSV via form/API → backend valida formato
  3. ✅ Backend processa CSV → genera JSON ottimizzato
  4. ✅ Ricevo conferma processamento con statistiche (record processati, errori)
  5. ✅ Applicazione frontend utilizza immediatamente nuovi dati (o dopo cache invalidation)
  6. ✅ 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 */ }

US-2: Consultazione Dati come Frontend Application

Come applicazione frontend
Voglio accedere ai dati tramite API
Così che possa visualizzare lemmi, aree geografiche e timeline

Acceptance Criteria:

  1. ✅ Chiamo API con API key valida → ricevo dati JSON
  2. ✅ API restituisce solo dati necessari (no metadati sensibili)
  3. ✅ Rate limiting previene download massivo
  4. ✅ 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",
    ...
  },
  ...
]

US-3: Audit e Monitoring come Admin

Come amministratore
Voglio visualizzare log di upload e accessi
Così che possa monitorare l'utilizzo e identificare anomalie

Acceptance Criteria:

  1. ✅ Visualizzazione log upload (chi, quando, risultato)
  2. ✅ Statistiche accessi API (rate, IP sorgente)
  3. ✅ Alert su upload falliti o accessi anomali

4. Protezione File CSV e JSON

4.1 Protezioni Implementate

Livello 1: Filesystem Permissions

# 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/*.csv

Risultato: Solo processo Node.js può leggere/scrivere file

Livello 2: Nginx Configuration

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)

Livello 3: Autenticazione Backend

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' });
  }
};

Livello 4: Rate Limiting

// 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 };

Livello 5: HTTPS Only (Produzione)

# 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
}

4.2 Matrice Protezione

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

4.3 Scenari di Attacco e Mitigazioni

Scenario 1: Tentativo Download Diretto JSON

Attacco: GET https://atliteg-map.com/data/lemmi.json

Mitigazione:

  1. Nginx blocca request → HTTP 403
  2. Log tentativo accesso (monitoring)
  3. File fisicamente non accessibile da web root

Risultato: ❌ Attacco fallito

Scenario 2: Scraping Massivo via API

Attacco: Script automatico chiama GET /api/lemmi ripetutamente

Mitigazione:

  1. Rate limiting: max 100 req/15min per IP
  2. Dopo 100 richieste → HTTP 429 Too Many Requests
  3. Possibile ban temporaneo IP (opzionale)
  4. Monitoring anomalie (spike richieste)

Risultato: ⚠️ Parzialmente mitigato (può ottenere dati ma lentamente)

Scenario 3: API Key Leak

Attacco: API key frontend esposta in codice sorgente

Mitigazione:

  1. API key rotazione periodica (es. mensile)
  2. Rate limiting per key
  3. Monitoring utilizzo per key
  4. Possibilità disabilitare key compromessa
  5. Nota: Protezione completa richiede auth utente (OAuth, etc.)

Risultato: ⚠️ Rischio residuo accettabile per applicazione pubblica

Scenario 4: Upload CSV Malicious

Attacco: Admin account compromesso, upload CSV con dati malevoli

Mitigazione:

  1. Validazione rigorosa CSV (schema, tipi dati, dimensione)
  2. Sanitizzazione input (rimozione caratteri speciali)
  3. Quarantena file: validazione prima di processare
  4. Rollback automatico su errori
  5. Audit completo: chi ha caricato cosa e quando
  6. Backup automatico file precedente

Risultato: ✅ Attacco mitigato con validazione e audit


5. Implementazione Dettagliata

5.1 API Endpoints

Endpoint: POST /api/admin/login

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;

Endpoint: POST /api/admin/upload

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'
      });
    }
  }
);

Endpoint: GET /api/admin/status/:jobId

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
  }
}

Endpoint: GET /api/lemmi

Scopo: Recupero dati lemmi (frontend)

Request:

GET /api/lemmi
X-API-Key: frontend_key_abc123xyz

Response:

[
  {
    "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;

5.2 Servizio Processamento CSV

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();

5.3 Configurazione Docker

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-stopped

File: .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

6. Workflow Completo

6.1 Setup Iniziale

# 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

6.2 Upload CSV (Prima Volta)

# 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 '.'

6.3 Consumo Dati Frontend

// 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;
  }
}

7. Testing e Validazione

7.1 Test Sicurezza

# 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

7.2 Test Funzionali

# 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 lemmi

8. Monitoraggio e Manutenzione

8.1 Logging

Winston 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"
}

8.2 Health Checks

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);
    });
});

8.3 Backup Strategy

#!/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>&1

9. Roadmap Implementazione

Fase 1: Backend Base (3 giorni)

Giorno 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

Fase 2: Integrazione Frontend (1 giorno)

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

Fase 3: Docker e Deploy (1 giorno)

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

Fase 4: Testing e Hardening (1 giorno - opzionale)

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

10. FAQ e Troubleshooting

Q1: Come ruoto le API keys del frontend?

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 backend

Q2: Cosa succede se l'upload CSV fallisce a metà?

A:

  • 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

Q3: Come gestisco dataset molto grandi (>100MB)?

A:

  • Aumentare maxFileSize in 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

Q4: Un utente può scaricare comunque i dati facendo molte richieste API?

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.

Q5: Come implemento rollback a versione precedente dati?

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

11. Conclusioni e Next Steps

Deliverable Finale

  1. Backend API completo con autenticazione e upload
  2. Frontend integrato che consuma API protette
  3. Docker deployment production-ready
  4. Documentazione completa per sviluppatori e operatori
  5. Script di backup e manutenzione
  6. Test suite per validazione sicurezza

Protezioni Implementate

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) ⚠️ Configurare

Metriche Successo

  • ⏱️ 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)

Prossimi Step Raccomandati

  1. Implementare backup automatico con retention policy
  2. Monitoring avanzato con Prometheus + Grafana (opzionale)
  3. OAuth2 login per admin (invece di username/password)
  4. Multi-tenancy se necessario gestire più dataset
  5. Versioning dati per storico modifiche
  6. API pagination per dataset molto grandi
  7. GraphQL endpoint (alternativa a REST, opzionale)

12. Stato Implementazione

12.1 Componenti Completati ✅

Backend API Server (Express.js)

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

Sicurezza e Autenticazione

  • 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

Processamento CSV

  • 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

Integrazione Frontend

Frontend aggiornato per usare backend API invece di file statici:

  • dataLoader.ts: Chiama /api/lemmi e /api/geojson
  • useRegions.ts: Chiama /api/regions
  • Variabili Ambiente: Aggiunti NEXT_PUBLIC_API_URL e NEXT_PUBLIC_API_KEY
  • Rimossi Fallback: Nessun accesso diretto ai file, solo chiamate API

Configurazione Docker

  • 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.conf aggiornato per:
    • Bloccare completamente directory /data/ (403 Forbidden)
    • Bloccare file .csv ovunque
    • Proxy richieste /api/ al backend:3001
  • docker-compose.yml: Orchestrazione entrambi i servizi con health checks

Protezione Dati

  • 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

12.2 Test Completati ✅

Test Backend Locali (Tutti Riusciti)

# 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, ...}

12.3 Configurazione Ambiente

Variabili Ambiente (.env)

# 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:3001

Setup Produzione

Per deployment in produzione, aggiornare .env con:

  1. API Keys forti: Generare con openssl rand -hex 16
  2. JWT Secret forte: Generare con openssl rand -hex 32
  3. Password Admin forte:
    node -e "require('bcrypt').hash('YOUR_PASSWORD', 10).then(h => console.log(h))"
  4. Domini effettivi per ALLOWED_ORIGINS

12.4 Struttura File Implementata

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

12.5 Attività Rimanenti ⏳

Verifica Docker Completa

# 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

Verifica Sicurezza

# 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

Test Applicazione

  1. Aprire browser: http://localhost:9000
  2. Verificare caricamento mappa con tutti i marker lemmi
  3. Verificare funzionamento filtri
  4. Verificare funzionalità ricerca
  5. Verificare assenza errori nella console browser
  6. Catturare screenshot

Test Upload CSV

  1. Login come admin per ottenere JWT token
  2. Upload nuovo file CSV
  3. Verificare stato job
  4. Verificare nuovi dati nell'applicazione
  5. Verificare creazione backup in server/uploads/backup/

12.6 Problemi Noti / TODO

  1. Build Docker Frontend: Build in corso quando interrotto il lavoro
  2. Test Frontend: Necessario verificare caricamento corretto applicazione con dati API
  3. Configurazione Produzione: Necessario impostare password e chiavi forti per produzione
  4. SSL/HTTPS: Non configurato (nginx.conf ha placeholder per HTTPS produzione)
  5. Persistenza Job: Job memorizzati in-memory (persi al restart). Considerare Redis per produzione
  6. UI Admin: Nessuna interfaccia admin creata (usare curl/Postman per ora)

12.7 Esempi Utilizzo API

Recupero Dati Lemmi

const response = await fetch('http://localhost:9000/api/lemmi', {
  headers: { 'X-API-Key': 'your_api_key' }
});
const lemmi = await response.json();

Upload CSV (come admin)

// 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();

12.8 Riferimenti

  • Design API Backend: Questo documento (sezioni 1-11)
  • Issue Originale: [problem statement in PR description]
  • File Esempio Ambiente: .env.example

13. Domande per Chiarimenti

Prima di procedere all'implementazione, confermare:

  1. Autenticazione admin: Username/password + JWT è sufficiente, o serve integrazione LDAP/OAuth?

  2. Rotazione API keys: Rotazione manuale periodica è accettabile, o serve automazione?

  3. Gestione file GeoJSON: Anche GeoJSON (Ambiti geolinguistici newline.json) deve essere generato da CSV, o può rimanere statico?

  4. Validazione CSV: Quali validazioni specifiche sul CSV? (es. campi obbligatori, formati, range valori)

  5. Notifiche upload: Email/Slack notification quando upload completato?

  6. Interfaccia admin: Serve UI web per upload, o CLI/API è sufficiente?

  7. Backup/Rollback: Backup automatico giornaliero è sufficiente? Serve rollback UI?

  8. Performance: Quanti utenti concorrenti previsti? (per dimensionare rate limits)

  9. Compliance: Requisiti GDPR/privacy specifici oltre quelli standard?

  10. Budget deployment: Hosting self-managed o cloud managed (AWS/Azure)?


Documento pronto per revisione e approvazione
Prossimo step: Feedback su requisiti → Implementazione Fase 1