feat: Controllo accessi RFID completo con gestione sessioni

- Aggiunto supporto multi-pattern RFID (US/IT layout)
- Implementata invalidazione sessioni al restart del server
- Schermata "badge non trovato" con countdown 30s
- Notifica quando badge validatore passato senza utente
- Database aggiornato con badge reali di test
- Layout ottimizzato per tablet orizzontale
- Banner NumLock per desktop
- Toggle visibilità password
- Carosello benvenuto multilingua (10 lingue)
- Pagina debug RFID (/debug)
This commit is contained in:
2026-01-17 20:06:50 +01:00
parent 21b509c6ba
commit e68f299feb
48 changed files with 3625 additions and 2445 deletions

View File

@@ -1,219 +1,335 @@
/**
* Focolari Voting System - RFID Scanner Hook
* Focolari Voting System - RFID Scanner Hook (v2 - Multi-Pattern)
*
* Questo hook gestisce la lettura di badge RFID tramite lettori USB
* che emulano una tastiera. Il protocollo prevede:
* - Carattere di inizio: `;`
* - Carattere di fine: `?`
* - Esempio: `;00012345?`
* che emulano una tastiera. Supporta pattern multipli per diversi layout:
* - Layout US: `;` → `?`
* - Layout IT: `ò` → `_`
*
* NOTA: Il lettore RFID invia Enter (\n) dopo l'ultimo carattere.
* L'hook lo gestisce ignorando l'Enter immediatamente dopo il completamento.
*
* L'hook funziona indipendentemente dal focus dell'applicazione.
*/
import { useState, useEffect, useCallback, useRef } from 'react';
import type { RFIDScannerState, RFIDScanResult } from '../types';
import {useCallback, useEffect, useRef, useState} from 'react';
import type {RFIDScannerState, RFIDScanResult} from '../types';
// Costanti
const START_SENTINEL = ';';
const END_SENTINEL = '?';
const TIMEOUT_MS = 3000; // 3 secondi di timeout
// ============================================
// CONFIGURAZIONE PATTERN RFID
// ============================================
export interface RFIDPattern {
name: string;
start: string;
end: string;
}
export const VALID_PATTERNS: RFIDPattern[] = [
{name: 'US', start: ';', end: '?'},
{name: 'IT', start: 'ò', end: '_'},
];
const TIMEOUT_MS = 2500; // 2.5 secondi di timeout
const ENTER_GRACE_PERIOD_MS = 100; // Ignora Enter entro 100ms dal completamento
// ============================================
// LOGGING
// ============================================
const log = (message: string, ...args: unknown[]) => {
console.log(`[RFID] ${message}`, ...args);
};
const logWarn = (message: string, ...args: unknown[]) => {
console.warn(`[RFID] ${message}`, ...args);
};
// ============================================
// TIPI
// ============================================
interface UseRFIDScannerOptions {
/** Callback chiamato quando un badge viene letto con successo */
onScan: (code: string) => void;
/** Callback opzionale chiamato in caso di timeout */
onTimeout?: () => void;
/** Callback opzionale chiamato quando inizia la scansione */
onScanStart?: () => void;
/** Se true, previene l'input nei campi di testo durante la scansione */
preventDefaultOnScan?: boolean;
/** Se true, l'hook è disabilitato */
disabled?: boolean;
/** Callback chiamato quando un badge viene letto con successo */
onScan: (code: string) => void;
/** Callback opzionale chiamato in caso di timeout */
onTimeout?: () => void;
/** Callback opzionale chiamato quando inizia la scansione */
onScanStart?: () => void;
/** Se true, previene l'input nei campi di testo durante la scansione */
preventDefaultOnScan?: boolean;
/** Se true, l'hook è disabilitato */
disabled?: boolean;
}
interface UseRFIDScannerReturn {
/** Stato corrente dello scanner */
state: RFIDScannerState;
/** Buffer corrente (solo per debug) */
buffer: string;
/** Ultimo codice scansionato */
lastScan: RFIDScanResult | null;
/** Reset manuale dello scanner */
reset: () => void;
export interface UseRFIDScannerReturn {
/** Stato corrente dello scanner */
state: RFIDScannerState;
/** Buffer corrente (solo per debug) */
buffer: string;
/** Ultimo codice scansionato */
lastScan: RFIDScanResult | null;
/** Pattern attualmente in uso (solo durante scanning) */
activePattern: RFIDPattern | null;
/** Reset manuale dello scanner */
reset: () => void;
/** Ultimi eventi tastiera (per debug) */
keyLog: KeyLogEntry[];
}
export interface KeyLogEntry {
key: string;
code: string;
timestamp: number;
}
const MAX_KEY_LOG = 20;
// ============================================
// HOOK
// ============================================
export function useRFIDScanner({
onScan,
onTimeout,
onScanStart,
preventDefaultOnScan = true,
disabled = false,
}: UseRFIDScannerOptions): UseRFIDScannerReturn {
const [state, setState] = useState<RFIDScannerState>('idle');
const [buffer, setBuffer] = useState<string>('');
const [lastScan, setLastScan] = useState<RFIDScanResult | null>(null);
onScan,
onTimeout,
onScanStart,
preventDefaultOnScan = true,
disabled = false,
}: UseRFIDScannerOptions): UseRFIDScannerReturn {
const [state, setState] = useState<RFIDScannerState>('idle');
const [buffer, setBuffer] = useState<string>('');
const [lastScan, setLastScan] = useState<RFIDScanResult | null>(null);
const [activePattern, setActivePattern] = useState<RFIDPattern | null>(null);
const [keyLog, setKeyLog] = useState<KeyLogEntry[]>([]);
// Refs per mantenere i valori aggiornati nei callback
const bufferRef = useRef<string>('');
const stateRef = useRef<RFIDScannerState>('idle');
const timeoutRef = useRef<number | null>(null);
// Refs per mantenere i valori aggiornati nei callback
const bufferRef = useRef<string>('');
const stateRef = useRef<RFIDScannerState>('idle');
const activePatternRef = useRef<RFIDPattern | null>(null);
const timeoutRef = useRef<number | null>(null);
const lastCompletionRef = useRef<number>(0);
// Sync refs con state
useEffect(() => {
bufferRef.current = buffer;
}, [buffer]);
// Sync refs con state
useEffect(() => {
bufferRef.current = buffer;
}, [buffer]);
useEffect(() => {
stateRef.current = state;
}, [state]);
useEffect(() => {
stateRef.current = state;
}, [state]);
/**
* Pulisce il timeout attivo
*/
const clearScanTimeout = useCallback(() => {
if (timeoutRef.current !== null) {
window.clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
}, []);
useEffect(() => {
activePatternRef.current = activePattern;
}, [activePattern]);
/**
* Resetta lo scanner allo stato idle
*/
const reset = useCallback(() => {
clearScanTimeout();
setState('idle');
setBuffer('');
bufferRef.current = '';
stateRef.current = 'idle';
}, [clearScanTimeout]);
/**
* Aggiunge un evento al log tastiera
*/
const addKeyLog = useCallback((key: string, code: string) => {
setKeyLog(prev => {
const newEntry: KeyLogEntry = {key, code, timestamp: Date.now()};
const updated = [newEntry, ...prev].slice(0, MAX_KEY_LOG);
return updated;
});
}, []);
/**
* Avvia il timeout di sicurezza
*/
const startTimeout = useCallback(() => {
clearScanTimeout();
timeoutRef.current = window.setTimeout(() => {
console.warn('[RFID Scanner] Timeout - lettura incompleta scartata');
onTimeout?.();
reset();
}, TIMEOUT_MS);
}, [clearScanTimeout, onTimeout, reset]);
/**
* Handler principale per gli eventi keydown
*/
useEffect(() => {
if (disabled) {
return;
}
const handleKeyDown = (event: KeyboardEvent) => {
const key = event.key;
// Ignora tasti speciali (frecce, funzione, ecc.)
if (key.length > 1 && key !== START_SENTINEL && key !== END_SENTINEL) {
// Eccezione per Backspace in stato scanning: ignora ma non resetta
if (key === 'Backspace' && stateRef.current === 'scanning') {
if (preventDefaultOnScan) {
event.preventDefault();
}
/**
* Pulisce il timeout attivo
*/
const clearScanTimeout = useCallback(() => {
if (timeoutRef.current !== null) {
window.clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
return;
}
}, []);
// STATO IDLE: attende il carattere di inizio
if (stateRef.current === 'idle') {
if (key === START_SENTINEL) {
console.log('[RFID Scanner] Start sentinel rilevato - inizio scansione');
/**
* Resetta lo scanner allo stato idle
*/
const reset = useCallback(() => {
clearScanTimeout();
setState('idle');
setBuffer('');
setActivePattern(null);
bufferRef.current = '';
stateRef.current = 'idle';
activePatternRef.current = null;
}, [clearScanTimeout]);
if (preventDefaultOnScan) {
event.preventDefault();
}
/**
* Avvia il timeout di sicurezza
*/
const startTimeout = useCallback(() => {
clearScanTimeout();
timeoutRef.current = window.setTimeout(() => {
logWarn('Buffer timeout - clearing data');
onTimeout?.();
reset();
}, TIMEOUT_MS);
}, [clearScanTimeout, onTimeout, reset]);
setState('scanning');
setBuffer('');
bufferRef.current = '';
startTimeout();
onScanStart?.();
}
// Altrimenti ignora il tasto (comportamento normale)
return;
}
/**
* Trova il pattern che corrisponde al carattere start
*/
const findPatternByStart = useCallback((char: string): RFIDPattern | undefined => {
return VALID_PATTERNS.find(p => p.start === char);
}, []);
// STATO SCANNING: accumula i caratteri o termina
if (stateRef.current === 'scanning') {
if (preventDefaultOnScan) {
event.preventDefault();
/**
* Handler principale per gli eventi keydown
*/
useEffect(() => {
if (disabled) {
return;
}
if (key === END_SENTINEL) {
// Fine della scansione
clearScanTimeout();
const handleKeyDown = (event: KeyboardEvent) => {
const key = event.key;
const code = event.code;
const scannedCode = bufferRef.current.trim();
// Log per debug
addKeyLog(key, code);
if (scannedCode.length > 0) {
console.log('[RFID Scanner] Codice scansionato:', scannedCode);
// Gestione Enter dopo completamento (grace period)
if (key === 'Enter' && Date.now() - lastCompletionRef.current < ENTER_GRACE_PERIOD_MS) {
log('Enter ignorato (post-completion grace period)');
if (preventDefaultOnScan) {
event.preventDefault();
}
return;
}
const result: RFIDScanResult = {
code: scannedCode,
timestamp: Date.now(),
};
// Gestione ESC: annulla scansione in corso
if (key === 'Escape' && stateRef.current === 'scanning') {
log('Scansione annullata con ESC');
if (preventDefaultOnScan) {
event.preventDefault();
}
reset();
return;
}
setLastScan(result);
onScan(scannedCode);
} else {
console.warn('[RFID Scanner] Codice vuoto scartato');
}
// Ignora tasti speciali (frecce, funzione, ecc.) ma non i sentinel
const isStartSentinel = VALID_PATTERNS.some(p => p.start === key);
const isEndSentinel = activePatternRef.current?.end === key;
reset();
} else if (key === START_SENTINEL) {
// Nuovo start sentinel durante scansione: resetta e ricomincia
console.log('[RFID Scanner] Nuovo start sentinel - reset buffer');
setBuffer('');
bufferRef.current = '';
startTimeout();
} else {
// Accumula il carattere nel buffer
const newBuffer = bufferRef.current + key;
setBuffer(newBuffer);
bufferRef.current = newBuffer;
}
}
if (key.length > 1 && !isStartSentinel && !isEndSentinel) {
// Eccezione per Backspace/Enter in stato scanning: ignora ma non resetta
if ((key === 'Backspace' || key === 'Enter') && stateRef.current === 'scanning') {
if (preventDefaultOnScan) {
event.preventDefault();
}
}
return;
}
// STATO IDLE: attende un carattere start di qualsiasi pattern
if (stateRef.current === 'idle') {
const pattern = findPatternByStart(key);
if (pattern) {
log(`Start sentinel detected: '${key}' (pattern ${pattern.name})`);
if (preventDefaultOnScan) {
event.preventDefault();
}
setState('scanning');
setActivePattern(pattern);
setBuffer('');
bufferRef.current = '';
activePatternRef.current = pattern;
startTimeout();
onScanStart?.();
}
// Altrimenti ignora il tasto (comportamento normale)
return;
}
// STATO SCANNING: accumula i caratteri o termina
if (stateRef.current === 'scanning') {
if (preventDefaultOnScan) {
event.preventDefault();
}
const currentPattern = activePatternRef.current;
// Verifica se è l'end sentinel del pattern attivo
if (currentPattern && key === currentPattern.end) {
// Fine della scansione
clearScanTimeout();
lastCompletionRef.current = Date.now();
const scannedCode = bufferRef.current.trim();
if (scannedCode.length > 0) {
log(`Scan complete (pattern ${currentPattern.name}): ${scannedCode}`);
const result: RFIDScanResult = {
code: scannedCode,
timestamp: Date.now(),
};
setLastScan(result);
onScan(scannedCode);
} else {
logWarn('Empty code discarded');
}
reset();
} else if (findPatternByStart(key)) {
// Nuovo start sentinel durante scansione: resetta e ricomincia con nuovo pattern
const newPattern = findPatternByStart(key)!;
log(`New start sentinel during scan - switching to pattern ${newPattern.name}`);
setBuffer('');
bufferRef.current = '';
setActivePattern(newPattern);
activePatternRef.current = newPattern;
startTimeout();
} else {
// Accumula il carattere nel buffer
const newBuffer = bufferRef.current + key;
setBuffer(newBuffer);
bufferRef.current = newBuffer;
}
}
};
// Aggiungi listener globale
window.addEventListener('keydown', handleKeyDown, {capture: true});
// Cleanup
return () => {
window.removeEventListener('keydown', handleKeyDown, {capture: true});
clearScanTimeout();
};
}, [
disabled,
onScan,
onScanStart,
preventDefaultOnScan,
clearScanTimeout,
reset,
startTimeout,
findPatternByStart,
addKeyLog,
]);
// Cleanup al unmount
useEffect(() => {
return () => {
clearScanTimeout();
};
}, [clearScanTimeout]);
return {
state,
buffer,
lastScan,
activePattern,
reset,
keyLog,
};
// Aggiungi listener globale
window.addEventListener('keydown', handleKeyDown, { capture: true });
// Cleanup
return () => {
window.removeEventListener('keydown', handleKeyDown, { capture: true });
clearScanTimeout();
};
}, [
disabled,
onScan,
onScanStart,
preventDefaultOnScan,
clearScanTimeout,
reset,
startTimeout,
]);
// Cleanup al unmount
useEffect(() => {
return () => {
clearScanTimeout();
};
}, [clearScanTimeout]);
return {
state,
buffer,
lastScan,
reset,
};
}
export default useRFIDScanner;