Sicurezza con il mio framework
Costruire API di Livello Enterprise con Astrazione, Sicurezza e Scalabilità
Se n8n orchestra il flusso di lavoro e FastAPI gestisce il carico pesante dell'NLP, allora NestJS è il guardiano al cancello. È il backend che autentica gli utenti, valida i permessi, gestisce lo stato e orchestra ogni interazione tra il frontend, i flussi di lavoro n8n e il livello dati.
Ma ecco il punto: costruire un backend sicuro e manutenibile non riguarda solo la validazione dei JWT o l'hashing delle password. Si tratta di farlo in modo coerente, ripetibile e senza reinventare la ruota.
È qui che entra in gioco il framework RAD-System: un insieme di pattern architetturali e classi base che trasforma i pattern aziendali comuni in astrazioni riutilizzabili. Questo articolo racconta come lo utilizzo per costruire un backend che scala con fiducia.
Il Livello Backend nello Stack RAG
Contestualizziamo NestJS all'interno del sistema RAG:
┌─────────────────────────────────────────────────────────────┐
│ Frontend Angular (rag-fe) │
│ - Interfaccia chat, dashboard amministrativi, login │
└─────────────────┬───────────────────────────────────────────┘
│ REST API + Autenticazione JWT
┌─────────────────▼───────────────────────────────────────────┐
│ Backend NestJS (rag-be) 🔒 │
│ - Auth, RBAC, servizi CRUD, trigger webhook, proxying │
└─────────────────┬───────────────────────────────────────────┘
│
┌─────────┴─────────┬─────────────────┐
│ │ │
┌───▼──┐ ┌─────▼─────┐ ┌────▼─────┐
│ n8n │ │ FastAPI │ │PostgreSQL│
│ │ │ (NLP) │ │(metadati)│
│ │ │ │ │ │
└───┬──┘ └─────┬─────┘ └────┬─────┘
│ │ │
└─────────────┬─────┴────────────────┘
│
┌───▼────┐
│ Qdrant │
│ (vettori)
└────────┘
Il backend NestJS è dove:
- Gli utenti si autenticano e ricevono token JWT
- I permessi vengono validati per ogni richiesta
- I documenti sono etichettati con proprietà e contesto
- Le chiamate webhook da n8n sono validate e protette
- Le risposte API sono formattate in modo coerente
- Gli errori sono gestiti in modo elegante
- I log delle modifiche sono mantenuti per audit trail
Perché il Framework RAD-System?
I backend aziendali risolvono gli stessi problemi ripetutamente:
Problema 1: Servizi DRY
Senza astrazione, ogni servizio ripete: trova, trova tutto, crea, aggiorna, elimina, rimuovi. Sono 6 funzioni × 20 entità = 120 metodi per lo più identici.
Problema 2: Controlli di Proprietà
Ogni azione deve verificare: "Questo utente possiede questa risorsa?" Che si tratti di documenti, chat o contesti, il pattern è identico ma facile da sbagliare.
Problema 3: Hashing delle Password
L'hashing bcrypt è semplice ma viene reimplementato in ogni servizio. Dimenticare di hashare una volta compromette la sicurezza.
Problema 4: Eliminazioni Soft
Gli audit trail richiedono eliminazioni soft (marcando deleted_at invece di rimuovere i record). Ma non tutti i sistemi ne hanno bisogno, ed è facile interrogare accidentalmente record eliminati.
La Soluzione: BaseEntity e BaseService
RAD-System fornisce classi base che gestiscono questi pattern:
BaseEntity: Timestamp, ID e Audit Trail
abstract class BaseEntity implements IBase {
@ApiProperty()
@CreateDateColumn({ name: 'created_at', type: 'timestamp' })
createdAt: Date;
@ApiProperty()
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
updatedAt?: Date;
@ApiProperty()
@DeleteDateColumn({ name: 'deleted_at', nullable: true })
deletedAt?: Date | null;
}
Ogni entità nel sistema RAG estende questa. Garantisce:
- Timestamp
created_at(immutabile) - Timestamp
updated_at(auto-aggiornato) - Timestamp
deleted_at(per eliminazioni soft)
Basta con le domande "quando è stato creato?"—è sempre lì.
BaseService: Operazioni CRUD e Controlli di Proprietà
abstract class BaseService {
constructor(protected readonly repository: Repository) {}
async findAll(options?: FindManyOptions): Promise {
return await this.repository.find(options);
}
async findOne(id: number, options?: FindOneOptions): Promise {
return await this.repository.findOne({ where: { id } });
}
async create(createDto: any): Promise {
// L'hashing delle password avviene qui se presente
if (createDto.password) {
createDto.password = await this.hashPassword(createDto.password);
}
const entity = this.repository.create(createDto);
return await this.repository.save(entity);
}
async update(id: number, updateDto: any): Promise {
if (updateDto.password) {
updateDto.password = await this.hashPassword(updateDto.password);
}
await this.repository.update(id, updateDto);
return this.findOne(id);
}
async findOneByUser(id: number, userId: number): Promise {
const hasAccess = await this.checkOwnership(id, userId);
if (!hasAccess) {
throw new ForbiddenException('Accesso negato');
}
return this.findOne(id);
}
protected abstract checkOwnership(id: number, userId: number): Promise;
}
Pattern chiave:
- CRUD DRY: Un'implementazione, riutilizzata ovunque
- Applicazione della proprietà: Ogni chiamata
findOneByUser()controlla i permessi - Sicurezza delle password: Hashing bcrypt automatico su creazione/aggiornamento
- Tipizzazione: Il generico
Tgarantisce correttezza a tempo di compilazione
Implementazione concreta per i Documenti:
@Injectable()
export class DocumentsService extends BaseService {
constructor(
@InjectRepository(Document) repository: Repository,
private usersService: UsersService
) {
super(repository);
}
protected async checkOwnership(id: number, userId: number): Promise {
const document = await this.repository.findOne({ where: { id } });
return document?.userId === userId;
}
}
Questo è tutto. Un metodo. Il resto è ereditato.
Autenticazione & Token JWT
Il flusso di autenticazione nel sistema RAG è centrato sui JWT (JSON Web Tokens) con supporto per il refresh token.
Flusso di Login
L'utente inserisce email + password
│
▼
POST /auth/login
│
▼
Validazione su database (o provider SSO)
│
▼
Generazione token JWT (access + refresh)
│
▼
Restituzione token al frontend
│
▼
Il frontend salva il refresh token in cookie httpOnly
Il frontend salva l'access token in memoria
Access Token: breve durata (1 ora), contiene ID utente e ruolo.
Refresh Token: lunga durata (7 giorni), salvato in cookie httpOnly, usato per generare nuovi access token.
Perché Due Token?
Se hai solo un token (breve + lunga TTL è contraddittorio):
- Solo breve durata: L'utente viene disconnesso dopo 1 ora. Frizione.
- Solo lunga durata: Furto del token = mesi di accesso compromesso.
Con entrambi:
- Access token (1 ora): Se rubato, il danno è limitato. Al massimo 1 ora.
- Refresh token (7 giorni): Salvato in cookie httpOnly (JavaScript non può accedervi). Se l'access token scade, ne ottieni uno nuovo.
Struttura del Payload JWT
interface JwtPayload {
sub: number; // Soggetto (ID utente)
email: string;
username: string;
role: 'USER' | 'ADMIN' | 'SUPERUSER';
permissions: string[]; // Derivato dal ruolo
iat: number; // Issued at
exp: number; // Expiration
}
Il campo role non basta per permessi complessi. L'array permissions contiene capacità specifiche (es. ['create:document', 'edit:own_documents', 'read:all']).
Integrazione SSO (Opzionale)
Il servizio di autenticazione supporta anche SSO (Single Sign-On):
async validateUser(emailOrUsername: string, password: string): Promise {
// Prova SSO prima (LDAP, Azure AD, ecc.)
const ssoUserData = await this.ssoService.authenticate(emailOrUsername, password);
if (ssoUserData) {
// Crea o aggiorna utente dai dati SSO
return await this.usersService.upsertFromSso(ssoUserData);
}
// Fallback su database locale
const user = await this.usersService.findByEmailOrUsername(emailOrUsername);
const isValid = await this.usersService.comparePasswords(password, user.password);
if (!isValid) return null;
return user;
}
Vantaggio: le organizzazioni possono collegare LDAP, Azure AD o SSO custom senza modifiche al codice.
Controllo Accessi Basato sui Ruoli (RBAC)
Tre ruoli formano la gerarchia dei permessi:
| Ruolo | Capacità |
|-------|----------|
| USER | Documenti propri, chat proprie, lettura contesti pubblici |
| ADMIN | Tutto user + gestione utenti, gestione documenti, gestione contesti |
| SUPERUSER | Tutto + configurazione sistema, audit trail, backup |
Ma i ruoli sono solo l'inizio. La vera magia sono i permessi contestuali.
Permessi Contestuali
Un "contesto" è un dominio di conoscenza (es. "Politiche HR", "Documentazione prodotto", "Wiki interno"). Gli utenti possono avere ruoli diversi in contesti diversi:
interface UserContextPermission {
userId: number;
contextId: number;
role: 'VIEWER' | 'EDITOR' | 'MANAGER';
createdAt: Date;
}
Esempi:
- Alice è ADMIN per "Politiche HR" → può aggiungere/rimuovere documenti
- Alice è VIEWER per "Documentazione prodotto" → può solo chattare
- Alice è USER normale per contesti personali
Permessi granulari senza esplosione di ruoli.
Guards: Applicazione dei Permessi
I Guards di NestJS intercettano le richieste e decidono se possono procedere:
@Controller('documents')
@UseGuards(JwtAuthGuard) // Deve essere loggato
export class DocumentsController {
@Get()
@UseGuards(CanReadDocumentsGuard) // Deve avere permesso di lettura
async findAll() { ... }
@Post()
@UseGuards(CanCreateDocumentsGuard) // Deve avere permesso di creazione
async create(@Body() createDto: CreateDocumentDto) { ... }
@Patch(':id')
@UseGuards(CanEditOwnDocumentsGuard) // Deve possedere il documento
async update(@Param('id') id: number, @Body() updateDto: UpdateDocumentDto) { ... }
}
Esempio di implementazione:
@Injectable()
export class CanEditOwnDocumentsGuard implements CanActivate {
constructor(
private documentsService: DocumentsService,
private permissionsService: PermissionsService
) {}
async canActivate(context: ExecutionContext): Promise {
const request = context.switchToHttp().getRequest();
const user = request.user; // Dal JWT decodificato da JwtAuthGuard
const documentId = request.params.id;
// Check 1: User è ADMIN → auto-consenti
if (user.role === 'ADMIN') return true;
// Check 2: User possiede il documento
const document = await this.documentsService.findOne(documentId);
if (document.userId === user.id) return true;
// Check 3: User ha permesso contestuale
return await this.permissionsService.hasPermission(
user.id,
document.contextId,
'EDITOR'
);
}
}
Questi guard si possono impilare: più guard su una route, tutti devono passare.
Documentazione API con Swagger/OpenAPI
Ogni backend NestJS dovrebbe essere autodocumentato. Il sistema RAG genera automaticamente la documentazione OpenAPI 3.0.
Nel Codice
I DTO (Data Transfer Object) sono decorati:
export class CreateDocumentDto {
@ApiProperty({ description: 'Titolo documento', example: 'Report finanziario Q2' })
title: string;
@ApiProperty({ description: 'Contenuto documento' })
content: string;
@ApiProperty({ description: 'ID contesto', example: 1 })
contextId: number;
}
I controller sono decorati:
@Controller('documents')
@ApiTags('Documents')
export class DocumentsController {
@Post()
@ApiOperation({ summary: 'Crea un nuovo documento' })
@ApiResponse({ status: 201, description: 'Documento creato', type: DocumentDto })
@ApiResponse({ status: 400, description: 'Input non valido' })
@ApiResponse({ status: 401, description: 'Non autorizzato' })
async create(@Body() createDto: CreateDocumentDto): Promise { ... }
}
Generazione Automatica
All'avvio, NestJS genera una OpenAPI spec completa e la espone su /api-docs/v1:
{
"openapi": "3.0.0",
"info": { "title": "RAG System API", "version": "1.0.0" },
"paths": {
"/documents": {
"post": {
"summary": "Crea un nuovo documento",
"requestBody": { ... },
"responses": { ... }
}
}
}
}
La Swagger UI (documentazione interattiva) è su /api-docs/.
I team frontend possono generare client TypeScript automaticamente da questa spec usando OpenAPI Generator. Niente più documentazione manuale degli endpoint.
Interceptors: Gestione Universale delle Richieste/Risposte
Gli Interceptor avvolgono ogni richiesta/risposta. Sono perfetti per le cross-cutting concern.
Logging delle Richieste
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('HTTP');
intercept(context: ExecutionContext, next: CallHandler): Observable {
const request = context.switchToHttp().getRequest();
const { method, url, user } = request;
this.logger.log(`${method} ${url} - User: ${user?.id}`);
return next.handle().pipe(
tap(() => this.logger.log(`${method} ${url} completed`)),
catchError(err => {
this.logger.error(`${method} ${url} failed: ${err.message}`);
throw err;
})
);
}
}
Formattazione delle Risposte
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable {
return next.handle().pipe(
map(data => ({
statusCode: context.switchToHttp().getResponse().statusCode,
message: 'Successo',
data,
timestamp: new Date().toISOString()
}))
);
}
}
Ora ogni risposta segue la stessa struttura:
{
"statusCode": 200,
"message": "Successo",
"data": { ... },
"timestamp": "2026-02-18T10:30:00.000Z"
}
Validation Pipes: Sanitizzazione Input
Prima di arrivare al controller, le richieste vengono validate:
export class CreateDocumentDto {
@IsString()
@MinLength(3)
@MaxLength(255)
title: string;
@IsString()
@MinLength(10)
content: string;
@IsNumber()
@IsPositive()
contextId: number;
}
Se la validazione fallisce:
{
"statusCode": 400,
"message": [
"title deve essere almeno di 3 caratteri",
"contextId deve essere un numero positivo"
],
"error": "Bad Request"
}
L'utente sa esattamente cosa non va.
Integrazione Webhook n8n
I documenti non si indicizzano da soli. Quando viene creato un nuovo documento, i workflow n8n devono saperlo.
Trigger Webhook da NestJS
@Controller('documents')
export class DocumentsController {
constructor(
private documentsService: DocumentsService,
private webhookService: WebhookService
) {}
@Post()
async create(@Body() createDto: CreateDocumentDto): Promise {
const document = await this.documentsService.create(createDto);
// Trigger workflow n8n
await this.webhookService.trigger('document.created', {
documentId: document.id,
title: document.title,
content: document.content,
contextId: document.contextId,
});
return document;
}
}
Il workflow n8n poi:
- Estrae il testo dal documento
- Lo suddivide in chunk semantici
- Genera embeddings
- Salva i vettori in Qdrant
- Aggiorna i metadati in PostgreSQL
Se qualcosa fallisce, il workflow lo logga e il documento viene marcato come "in attesa di indicizzazione". Nessun errore silenzioso.
Design Database-Agnostico
TypeORM astrae le differenze tra database. Lo stesso codice funziona con:
- PostgreSQL (produzione)
- MySQL (compatibile)
- SQLite (sviluppo locale)
- MariaDB (alternativa enterprise)
Basta cambiare ormconfig.json:
{
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "rag_user",
"password": "secure_password",
"database": "rag_system",
"entities": ["src/**/*.entity.ts"],
"migrations": ["src/migrations/**/*.ts"],
"synchronize": false
}
Nessuna query specifica TypeORM nel codice. Nessuna stored procedure. Solo:
await this.repository.find({ where: { contextId: id } });
Funziona identicamente su Postgres, MySQL o SQLite.
Soft Delete e Audit Trail
Invece di DELETE, il sistema usa soft delete:
// Vecchio modo (hard delete)
DELETE FROM documents WHERE id = 5;
// Nuovo modo (soft delete)
UPDATE documents SET deleted_at = NOW() WHERE id = 5;
Perché?
- Audit trail: "Cosa è successo a questo documento?" Controlla il timestamp deleted_at.
- Recupero: "Ops, ho eliminato il documento sbagliato." Facile ripristino—basta impostare deleted_at a NULL.
- Integrità referenziale: Le chiavi esterne verso record eliminati funzionano (il record esiste, solo marcato come eliminato).
Le query escludono automaticamente i record soft-deleted:
async findAll(): Promise {
return this.repository.find({
where: { deletedAt: IsNull() } // Solo non eliminati
});
}
Per audit admin, includi anche i record eliminati:
async findAllWithDeleted(): Promise {
return this.repository.find({
withDeleted: true // Include soft-deleted
});
}
Gestione della Configurazione
Le variabili d'ambiente configurano tutto:
NODE_ENV=production
DATABASE_URL=postgres://user:pass@db:5432/rag
JWT_SECRET=your_secret_key_here
JWT_EXPIRY=1h
REFRESH_TOKEN_EXPIRY=7d
N8N_WEBHOOK_URL=http://n8n:5678/webhook/rag
FASTAPI_URL=http://fastapi:8000
REDIS_URL=redis://redis:6379
Caricate tramite ConfigService:
@Injectable()
export class AppService {
constructor(private configService: ConfigService) {}
isDevelopment(): boolean {
return this.configService.get('NODE_ENV') === 'development';
}
getJwtSecret(): string {
return this.configService.get('JWT_SECRET');
}
}
Nessun valore hardcoded. I segreti non sono nel git.
Gestione Errori: Risposte Coerenti
Tutti gli errori seguono la stessa struttura:
import { HttpException, HttpStatus } from '@nestjs/common';
throw new HttpException(
{
statusCode: HttpStatus.BAD_REQUEST,
message: 'Il titolo del documento è obbligatorio',
error: 'ValidationError'
},
HttpStatus.BAD_REQUEST
);
Risposta:
{
"statusCode": 400,
"message": "Il titolo del documento è obbligatorio",
"error": "ValidationError",
"timestamp": "2026-02-18T10:35:00.000Z"
}
Il client sa sempre:
- Codice HTTP
- Messaggio leggibile
- Categoria errore
- Quando è successo
Vantaggi RAD-System: Da Giorni a Ore
Quantifichiamo il risparmio di tempo con RAD-System:
Creare una nuova entità senza RAD-System:
- Crea classe entità (decoratori TypeORM, relazioni) → 30 min
- Crea DTO (shape richiesta/risposta) → 20 min
- Crea servizio (CRUD, controlli proprietà) → 60 min
- Crea controller (endpoint, validazione, gestione errori) → 45 min
- Crea guard (controlli permessi) → 30 min
- Aggiungi al modulo → 10 min
Totale: 195 minuti (3+ ore)
Creare la stessa con RAD-System:
- Crea entità (estendi BaseEntity, aggiungi campi) → 15 min
- Crea DTO → 15 min
- Crea servizio (estendi BaseService, implementa checkOwnership) → 10 min
- Crea controller (riutilizza route standard) → 15 min
- Riutilizza guard esistenti → 0 min
- Aggiungi al modulo → 5 min
Totale: 60 minuti (1 ora)
Un risparmio di 3.25x. Su un backend con 20 entità, sono 15+ ore risparmiate.
Prossimi Passi
Il backend NestJS è la base. Nel prossimo articolo esploreremo come il frontend Angular consuma queste API, gestisce lo stato e implementa l'interfaccia chat RAG.
Poi affronteremo il cambio provider LLM: come basta una configurazione per passare da OpenAI a Ollama locale senza ridistribuire.
---
Key Takeaways:
✅ BaseEntity + BaseService = CRUD DRY, sicuro, manutenibile
✅ Token JWT = Autenticazione stateless e scalabile
✅ Guard + Interceptor = Enforcement centralizzato dei permessi
✅ TypeORM = Database-agnostico, nessun lock-in
✅ Soft delete = Audit trail + facile recupero
✅ Documentazione API = Auto-generata, sempre aggiornata
✅ Integrazione n8n = Workflow trigger su cambi dati
Il backend è noioso. Ed è un bene. Noioso significa affidabile, scalabile e permette al team di concentrarsi sulle funzionalità invece che sull'infrastruttura.
---
GitHub:
- RAD System (open-source): Github source code
- RAG System (codice non disponibile): Github RAG System Overview