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