Merge pull request '0002566-lettura-tessera-dematerializzata-tramite-QR-code' (#8) from 0002566-lettura-tessera-dematerializzata-tramite-QR-code into dev
Reviewed-on: #8
This commit is contained in:
commit
4489f3d311
@ -73,6 +73,8 @@
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" attributes="0">
|
||||
<Component id="lblCF" min="-2" max="-2" attributes="0"/>
|
||||
<EmptySpace min="-2" pref="61" max="-2" attributes="0"/>
|
||||
<Component id="jtxtNumTesseraHidden" min="-2" max="-2" attributes="0"/>
|
||||
<EmptySpace max="32767" attributes="0"/>
|
||||
</Group>
|
||||
<Group type="102" attributes="0">
|
||||
@ -98,6 +100,8 @@
|
||||
<DimensionLayout dim="1">
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" alignment="1" attributes="0">
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" attributes="0">
|
||||
<Component id="jLblTitolo" min="-2" pref="25" max="-2" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="3" attributes="0">
|
||||
@ -105,7 +109,13 @@
|
||||
<Component id="lblNome" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="lblCF" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<Group type="102" alignment="0" attributes="0">
|
||||
<EmptySpace min="-2" pref="18" max="-2" attributes="0"/>
|
||||
<Component id="jtxtNumTesseraHidden" min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
</Group>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="0" max="-2" attributes="0">
|
||||
<Group type="103" groupAlignment="3" attributes="0">
|
||||
<Component id="jtxtNumTessera" alignment="3" min="-2" pref="40" max="-2" attributes="0"/>
|
||||
@ -263,6 +273,13 @@
|
||||
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="jbtnGiuActionPerformed"/>
|
||||
</Events>
|
||||
</Component>
|
||||
<Component class="javax.swing.JTextField" name="jtxtNumTesseraHidden">
|
||||
<Properties>
|
||||
<Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
|
||||
<Dimension value="[0, 0]"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
</Component>
|
||||
</SubComponents>
|
||||
</Container>
|
||||
</SubComponents>
|
||||
|
@ -6,24 +6,38 @@
|
||||
|
||||
package puntocassa;
|
||||
|
||||
import com.github.sarxos.webcam.Webcam;
|
||||
import com.google.zxing.BinaryBitmap;
|
||||
import com.google.zxing.LuminanceSource;
|
||||
import com.google.zxing.MultiFormatReader;
|
||||
import com.google.zxing.NotFoundException;
|
||||
import com.google.zxing.Result;
|
||||
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
|
||||
import com.google.zxing.common.HybridBinarizer;
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Font;
|
||||
import java.awt.Image;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.event.FocusAdapter;
|
||||
import java.awt.event.FocusEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.List;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.ListSelectionModel;
|
||||
import javax.swing.table.TableColumn;
|
||||
import puntocassa.utils.QrCryptoService;
|
||||
import puntocassa.utils.Utils;
|
||||
|
||||
/**
|
||||
@ -43,6 +57,7 @@ private JTextField jtxtTessera;
|
||||
private Boolean FlagMostraDataNascita=true;
|
||||
private Boolean DisabilitaPerBuono=false;
|
||||
|
||||
|
||||
/**
|
||||
* Creates new form frm
|
||||
*/
|
||||
@ -102,6 +117,43 @@ private JTextField jtxtTessera;
|
||||
Disabilita();
|
||||
}
|
||||
|
||||
|
||||
|
||||
jtxtNumTesseraHidden.addActionListener(e -> {
|
||||
System.out.println("QR letto: " + jtxtNumTesseraHidden.getText().trim());
|
||||
String valueRead = jtxtNumTesseraHidden.getText().trim();
|
||||
//decripting
|
||||
List<String> dataResult = QrCryptoService.decryptAndValidate(valueRead);
|
||||
//System.out.println("QR code decriptato: " + r);
|
||||
if(dataResult.get(0).equals("VALID")){
|
||||
//verifica che il codice fiscale corrisponda ad una tessera
|
||||
String numTessera = Utils.mySelect("select numero from tessere where id_utente = (select id from utenti where codice_fiscale = '" +
|
||||
dataResult.get(1) +
|
||||
"')", "numero", frmPuntoCassa);
|
||||
if(numTessera == null || "".equals(numTessera)){
|
||||
//mostra un popup di errore UTENTE NON COLLEGATO AD UNA TESSERA
|
||||
System.out.println("nessuna tessera collegata all'utente " + dataResult.get(1));
|
||||
JOptionPane.showOptionDialog(this, "Nessuna tessera collegata all'utente " + dataResult.get(1) , "Attenzione",
|
||||
JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, null, null);
|
||||
jtxtNumTesseraHidden.setText("");
|
||||
jtxtNumTessera.setText("");
|
||||
return;
|
||||
}
|
||||
//scrivi il numero della tessera nel campo input
|
||||
jtxtNumTessera.setText(numTessera);
|
||||
jtxtNumTesseraHidden.setText("");
|
||||
Cerca();
|
||||
}
|
||||
else {
|
||||
System.out.println("QR non valido: "+ dataResult.get(1));
|
||||
JOptionPane.showOptionDialog(this, dataResult.get(1) , "Attenzione",
|
||||
JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, null, null);
|
||||
jtxtNumTesseraHidden.setText("");
|
||||
jtxtNumTessera.setText("");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -128,6 +180,7 @@ private JTextField jtxtTessera;
|
||||
jBtnOK = new javax.swing.JButton();
|
||||
jbtnSu = new javax.swing.JButton();
|
||||
jbtnGiu = new javax.swing.JButton();
|
||||
jtxtNumTesseraHidden = new javax.swing.JTextField();
|
||||
|
||||
setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
|
||||
setTitle("Cerca Tessera Attiva");
|
||||
@ -237,6 +290,8 @@ private JTextField jtxtTessera;
|
||||
}
|
||||
});
|
||||
|
||||
jtxtNumTesseraHidden.setPreferredSize(new java.awt.Dimension(0, 0));
|
||||
|
||||
javax.swing.GroupLayout pnl1Layout = new javax.swing.GroupLayout(pnl1);
|
||||
pnl1.setLayout(pnl1Layout);
|
||||
pnl1Layout.setHorizontalGroup(
|
||||
@ -256,6 +311,8 @@ private JTextField jtxtTessera;
|
||||
.addGroup(pnl1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(pnl1Layout.createSequentialGroup()
|
||||
.addComponent(lblCF)
|
||||
.addGap(61, 61, 61)
|
||||
.addComponent(jtxtNumTesseraHidden, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addContainerGap())
|
||||
.addGroup(pnl1Layout.createSequentialGroup()
|
||||
.addComponent(jtxtNumTessera, javax.swing.GroupLayout.PREFERRED_SIZE, 176, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
@ -274,12 +331,17 @@ private JTextField jtxtTessera;
|
||||
pnl1Layout.setVerticalGroup(
|
||||
pnl1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, pnl1Layout.createSequentialGroup()
|
||||
.addGroup(pnl1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(pnl1Layout.createSequentialGroup()
|
||||
.addComponent(jLblTitolo, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addGroup(pnl1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
|
||||
.addComponent(lblCognome)
|
||||
.addComponent(lblNome)
|
||||
.addComponent(lblCF))
|
||||
.addComponent(lblCF)))
|
||||
.addGroup(pnl1Layout.createSequentialGroup()
|
||||
.addGap(18, 18, 18)
|
||||
.addComponent(jtxtNumTesseraHidden, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addGroup(pnl1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
|
||||
.addGroup(pnl1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
|
||||
@ -330,6 +392,9 @@ private JTextField jtxtTessera;
|
||||
jtxtNome.setBackground(Color.white);
|
||||
jtxtCognome.setBackground(Color.white);
|
||||
}
|
||||
|
||||
jtxtNumTesseraHidden.requestFocusInWindow();
|
||||
|
||||
// MyApplication MyA = new MyApplication();
|
||||
// if (MyA.TastieraVideo) {
|
||||
// if (MyA.UltimoTxt.equalsIgnoreCase(evt.getComponent().toString())) {
|
||||
@ -478,6 +543,54 @@ private JTextField jtxtTessera;
|
||||
|
||||
private void jtxtNumTesseraMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_jtxtNumTesseraMouseClicked
|
||||
Tastiera(evt);
|
||||
|
||||
// //attivazione webcam per lettura qr
|
||||
// Webcam webcam = Webcam.getDefault();
|
||||
// //webcam.setViewSize(new Dimension(1280, 720));
|
||||
// Dimension hd = new Dimension(1280, 720);
|
||||
// webcam.setCustomViewSizes(new Dimension[] { hd });
|
||||
// webcam.setViewSize(hd);
|
||||
//
|
||||
// webcam.open();
|
||||
//
|
||||
// while (true) {
|
||||
// BufferedImage image = webcam.getImage();
|
||||
// if (image == null) continue;
|
||||
//
|
||||
// LuminanceSource source = new BufferedImageLuminanceSource(image);
|
||||
// BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
//
|
||||
// try {
|
||||
// Result result = new MultiFormatReader().decode(bitmap);
|
||||
// System.out.println("QR Code trovato: " + result.getText());
|
||||
// //decripting
|
||||
// List<String> dataResult = QrCryptoService.decryptAndValidate(result.getText());
|
||||
// //System.out.println("QR code decriptato: " + r);
|
||||
// if(dataResult.get(0).equals("VALID")){
|
||||
// //verifica che il codice fiscale corrisponda ad una tessera
|
||||
// String numTessera = Utils.mySelect("select numero from tessere where id_utente = (select id from utenti where codice_fiscale = '" +
|
||||
// dataResult.get(1) +
|
||||
// "')", "numero", frmPuntoCassa);
|
||||
// if(numTessera == null || "".equals(numTessera)){
|
||||
// //mostra un popup di errore UTENTE NON COLLEGATO AD UNA TESSERA
|
||||
// System.out.println("nessuna tessera collegata all'utente " + dataResult.get(1));
|
||||
// return;
|
||||
// }
|
||||
// //scrivi il numero della tessera nel campo input
|
||||
// jtxtNumTessera.setText(numTessera);
|
||||
// Cerca();
|
||||
// }
|
||||
// else {
|
||||
// System.out.println("QR non valido: "+ dataResult.get(1));
|
||||
// }
|
||||
// break;
|
||||
// } catch (NotFoundException e) {
|
||||
// // Nessun QR code trovato nel frame
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// webcam.close();
|
||||
|
||||
}//GEN-LAST:event_jtxtNumTesseraMouseClicked
|
||||
private void Tastiera(java.awt.event.MouseEvent evt) {
|
||||
MyApplication MyA = new MyApplication();
|
||||
@ -753,6 +866,7 @@ private JTextField jtxtTessera;
|
||||
private javax.swing.JTextField jtxtCognome;
|
||||
private javax.swing.JTextField jtxtNome;
|
||||
private javax.swing.JTextField jtxtNumTessera;
|
||||
private javax.swing.JTextField jtxtNumTesseraHidden;
|
||||
private javax.swing.JLabel lblCF;
|
||||
private javax.swing.JLabel lblCognome;
|
||||
private javax.swing.JLabel lblNome;
|
||||
|
@ -27,10 +27,13 @@ import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import javax.swing.DefaultComboBoxModel;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JTextField;
|
||||
@ -49,6 +52,7 @@ import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import puntocassa.utils.QrCryptoService;
|
||||
import puntocassa.utils.Utils;
|
||||
|
||||
//******************************************************************************
|
||||
@ -226,7 +230,6 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
|
||||
settingFontsInterfaccia();
|
||||
|
||||
|
||||
jcmbSmartCard.setVisible(false);
|
||||
|
||||
visualizzaMessaggiDisplay = document.getElementsByTagName("Visualizza_Messaggi_Display").item(0).getFirstChild().getNodeValue()
|
||||
@ -236,7 +239,6 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
|
||||
aggiornaSmartCardReaderCombo(myApp);
|
||||
|
||||
|
||||
String ultimiMovimenti = Utils.mySelect("SELECT valore FROM parametri WHERE chiave = 'FlagVisualizzaUltimiPassaggi'",
|
||||
"valore",
|
||||
this);
|
||||
@ -314,7 +316,6 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
jTxtTessera.setFont(f);
|
||||
jChLogin.setFont(f);
|
||||
|
||||
|
||||
Font newFontBtnAnnulla = new Font(jbtnAnnulla.getFont().getName(), jbtnAnnulla.getFont().getStyle(), hFont);
|
||||
jbtnAnnulla.setFont(newFontBtnAnnulla);
|
||||
Font newFontBtnDuplica = new Font(jbtnDuplica.getFont().getName(), jbtnDuplica.getFont().getStyle(), hFont);
|
||||
@ -331,6 +332,7 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
|
||||
/**
|
||||
* Lettura del nodo 'display' del file di configurazione
|
||||
*
|
||||
* @param document
|
||||
* @param myApp
|
||||
* @throws NumberFormatException
|
||||
@ -508,8 +510,10 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
//29-10-18------------------------------------------------------
|
||||
if (timerUnaVolta == false) {
|
||||
timerUnaVolta = true;
|
||||
|
||||
final Timer timer = new Timer();
|
||||
final TimerTask task = new TimerTask() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
@ -526,12 +530,11 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
}
|
||||
};
|
||||
|
||||
timer.schedule(task, 5000);
|
||||
timer.schedule(task, 4000);
|
||||
}
|
||||
//fine 29-10-18------------------------------------------------------
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -553,7 +556,15 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
@Override
|
||||
public void run() {
|
||||
String tessera = "";
|
||||
boolean isQrCode = false;
|
||||
//se abbiamo uno \n e la lunghezza è > 200 caratteri, è un qr code
|
||||
if (testoSwipCard.toString().contains("\n") && testoSwipCard.length() > 200) {
|
||||
recuperaNumeroTesseraQrCode();
|
||||
isQrCode = true;
|
||||
|
||||
}
|
||||
//per tessere tipo 8 e 9
|
||||
if (!isQrCode) {
|
||||
if (testoSwipCard.toString().contains("ì")) {
|
||||
String[] txt = testoSwipCard.toString().split("ì");
|
||||
|
||||
@ -607,20 +618,180 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
|
||||
testoSwipCard = new StringBuilder();
|
||||
|
||||
}
|
||||
|
||||
timer.cancel();
|
||||
timer.purge();
|
||||
statoCardLettore = false;
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
timer.schedule(task, delaySwipCard);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
// public KeyEventDispatcher creaKeyEventDispatcher() {
|
||||
// return new KeyEventDispatcher() {
|
||||
// Boolean timerUnaVolta = false;
|
||||
// MyApplication myApp = new MyApplication();
|
||||
//
|
||||
// public boolean dispatchKeyEvent(KeyEvent e) {
|
||||
// int id = e.getID();
|
||||
//
|
||||
// if (jbtnChiudi.isEnabled() && !pnlLogin.isVisible() && jTxtTessera.getText().length() > 0) {
|
||||
// e.consume();
|
||||
// if (!myApp.unaVolta) {
|
||||
// myApp.unaVolta = true;
|
||||
// }
|
||||
// return false;
|
||||
// }
|
||||
// myApp.unaVolta = false;
|
||||
//
|
||||
// int keyCode = e.getKeyCode();
|
||||
// if (id == KeyEvent.KEY_RELEASED) {
|
||||
// String tasto = "" + e.getKeyChar();
|
||||
// System.out.println(keyCode + " - " + tasto);
|
||||
//
|
||||
// logTestoTessera.append(e.getKeyChar());
|
||||
//
|
||||
// // RFID input
|
||||
// if (keyCode != KeyEvent.VK_SHIFT) {
|
||||
// testoRFIDCard.append(tasto);
|
||||
// }
|
||||
//
|
||||
// // Invio: fine lettura RFID
|
||||
// if (keyCode == KeyEvent.VK_ENTER) {
|
||||
// System.out.println(testoRFIDCard.toString());
|
||||
// warn();
|
||||
// }
|
||||
//
|
||||
// // Timer per letture incomplete
|
||||
// if (!timerUnaVolta) {
|
||||
// timerUnaVolta = true;
|
||||
//
|
||||
// final Timer timer = new Timer();
|
||||
// final TimerTask task = new TimerTask() {
|
||||
// @Override
|
||||
// public void run() {
|
||||
// if (testoRFIDCard.toString().length() > 2) {
|
||||
// System.out.println("Timer: " + testoRFIDCard.toString());
|
||||
// warn();
|
||||
// testoRFIDCard = new StringBuilder();
|
||||
// }
|
||||
//
|
||||
// timer.cancel();
|
||||
// timer.purge();
|
||||
// timerUnaVolta = false;
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// timer.schedule(task, 4000);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return true;
|
||||
// }
|
||||
//
|
||||
// public void warn() {
|
||||
// if (statoCardLettore) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// //testoRFIDCard = new StringBuilder();
|
||||
//
|
||||
// if (testoRFIDCard.toString().length() > 0) {
|
||||
// statoCardLettore = true;
|
||||
//
|
||||
// final Timer timer = new Timer();
|
||||
// final TimerTask task = new TimerTask() {
|
||||
// @Override
|
||||
// public void run() {
|
||||
// String tessera = "";
|
||||
// boolean isQrCode = false;
|
||||
//
|
||||
// if (testoRFIDCard.toString().contains("\n") && testoRFIDCard.length() > 200) {
|
||||
// recuperaNumeroTesseraQrCode();
|
||||
// isQrCode = true;
|
||||
// }
|
||||
//
|
||||
// if (!isQrCode) {
|
||||
// tessera = testoRFIDCard.toString().trim().replace("_", "");
|
||||
//
|
||||
// if (tessera.length() >= 17 && tessera.length() <= 19) {
|
||||
// tessera = tessera.substring(0, 7);
|
||||
// }
|
||||
//
|
||||
// if (tessera.length() == 21) {
|
||||
// tessera = tessera.substring(0, 10);
|
||||
// int posI = 0;
|
||||
// for (int i = 0; i <= tessera.length(); i++) {
|
||||
// if (!tessera.substring(i, i + 1).equalsIgnoreCase("0")) {
|
||||
// posI = i;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// tessera = tessera.substring(posI);
|
||||
// }
|
||||
//
|
||||
// if (pnlLogin.isVisible()) {
|
||||
// controllaLogInCarta(tessera);
|
||||
// } else {
|
||||
// jTxtTessera.setText(tessera);
|
||||
// passaggioTessera = true;
|
||||
// logNumeroTessera = tessera;
|
||||
// cercaTessera();
|
||||
// }
|
||||
//
|
||||
// testoRFIDCard = new StringBuilder();
|
||||
// }
|
||||
//
|
||||
// timer.cancel();
|
||||
// timer.purge();
|
||||
// statoCardLettore = false;
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// timer.schedule(task, delaySwipCard);
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
|
||||
public void recuperaNumeroTesseraQrCode() {
|
||||
//controllo sul qr code prima di lanciare la ricerca della tessera
|
||||
String valueRead = testoSwipCard.toString().trim();
|
||||
//String valueRead = testoRFIDCard.toString().trim();
|
||||
//decripting
|
||||
java.util.List<String> dataResult = QrCryptoService.decryptAndValidate(valueRead);
|
||||
if (dataResult.get(0).equals("VALID")) {
|
||||
//verifica che il codice fiscale corrisponda ad una tessera
|
||||
String numTessera = Utils.mySelect("select seleziona_tessera ('" + dataResult.get(1) + "') as numero from dual", "numero", this);
|
||||
System.out.println("Dopo select numero from tessere");
|
||||
if (numTessera == null || "".equals(numTessera)) {
|
||||
//mostra un popup di errore UTENTE NON COLLEGATO AD UNA TESSERA
|
||||
System.out.println("nessuna tessera collegata all'utente " + dataResult.get(1));
|
||||
JOptionPane.showOptionDialog(this, "Nessuna tessera collegata all'utente " + dataResult.get(1), "Attenzione",
|
||||
JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, null, null);
|
||||
jTxtTessera.setText("");
|
||||
}
|
||||
//scrivi il numero della tessera nel campo input
|
||||
jTxtTessera.setText(numTessera);
|
||||
cercaTessera();
|
||||
} else {
|
||||
System.out.println("Errore validazione QR code: " + dataResult.get(1));
|
||||
JOptionPane.showOptionDialog(this, dataResult.get(1), "Attenzione",
|
||||
JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, null, null);
|
||||
//jtxtNumTesseraHidden.setText("");
|
||||
jTxtTessera.setText("");
|
||||
//reset del logTestoTessera in quanto non viene richiamata la cercaTessera() che contiene logTessera() che effettua la stessa istruzione
|
||||
logTestoTessera = new StringBuilder();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* *
|
||||
@ -701,6 +872,7 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
|
||||
/**
|
||||
* Aggiorna la lista di lettori smartcard disponibili
|
||||
*
|
||||
* @param myApp
|
||||
*/
|
||||
private void aggiornaSmartCardReaderCombo(MyApplication myApp) {
|
||||
@ -1827,8 +1999,8 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
//inserisci i prodotti se la cassa è aperta e
|
||||
//non c'è una prenotazione da gestire oppure
|
||||
//c'è una prenotazione e si stanno inserendo i prodotti in lista (idPrenDettaglio viene valorizzato in inserisciPrenotazioni())
|
||||
if (isCassaAperta &&
|
||||
(idPrenotazione == 0 || !idPrenDettaglio.equals("null"))) {
|
||||
if (isCassaAperta
|
||||
&& (idPrenotazione == 0 || !idPrenDettaglio.equals("null"))) {
|
||||
sql = "SELECT "
|
||||
+ "prodotti.id as idProdotto,"
|
||||
+ "prodotti.nome, "
|
||||
@ -1842,8 +2014,6 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
+ "where prodotti.id = " + id + " and Tariffe.id_fascia=" + idProfiloTariffario;
|
||||
try {
|
||||
|
||||
|
||||
|
||||
//spengo altri togglebutton
|
||||
for (Integer y = 0; y < this.pnlProdotti.getComponentCount(); y++) {
|
||||
JToggleButton bt = (JToggleButton) this.pnlProdotti.getComponent(y);
|
||||
@ -1896,10 +2066,11 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
}
|
||||
tblListaProdotti.setModel(model);
|
||||
|
||||
if(idPrenotazione == 0)
|
||||
if (idPrenotazione == 0) {
|
||||
controllaCompleti(model, "null");
|
||||
else
|
||||
} else {
|
||||
controllaCompleti(model, idPrenotazione.toString());
|
||||
}
|
||||
controllaExtra(model);
|
||||
sommaColonne(model, flagPrenotazionePagata);
|
||||
|
||||
@ -1928,8 +2099,7 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
} catch (Exception ex) {
|
||||
System.out.println(ex.getMessage());
|
||||
}
|
||||
}
|
||||
else if(idPrenotazione > 0 && idPrenDettaglio.equals("null")){
|
||||
} else if (idPrenotazione > 0 && idPrenDettaglio.equals("null")) {
|
||||
JOptionPane.showMessageDialog(this, "Non è possibile modificare prodotti e composizioni se è presente una prenotazione collegata alla tessera");
|
||||
}
|
||||
}
|
||||
@ -1995,10 +2165,10 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
|
||||
/**
|
||||
* funzione che calcola i prezzi dei prodotti e il totale in base alla
|
||||
* fascia dello studente oppure utilizza la fascia di default.
|
||||
* Se flagPrenotazionePagata = true vuol dire che non va modificato il saldo
|
||||
* residuo della tessera perchè la somma delle colonne viene richiamata
|
||||
* a seguito della verifica di una prenotazione pagata
|
||||
* fascia dello studente oppure utilizza la fascia di default. Se
|
||||
* flagPrenotazionePagata = true vuol dire che non va modificato il saldo
|
||||
* residuo della tessera perchè la somma delle colonne viene richiamata a
|
||||
* seguito della verifica di una prenotazione pagata
|
||||
*
|
||||
* @param model
|
||||
* @param flagPrenotazionePagata
|
||||
@ -2011,8 +2181,9 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
}
|
||||
|
||||
DecimalFormat df2 = new DecimalFormat("#,###,###,##0.00");
|
||||
if(!flagPrenotazionePagata)
|
||||
if (!flagPrenotazionePagata) {
|
||||
txtTotaleCassa.setText("€ " + df2.format(t));
|
||||
}
|
||||
|
||||
int p = model.sommaInt(2);
|
||||
txtTotalePunti.setText("Punti " + p);
|
||||
@ -2836,8 +3007,7 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
private void jbtnEliminaActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jbtnEliminaActionPerformed
|
||||
if (idPrenotazione > 0) {
|
||||
JOptionPane.showMessageDialog(this, "Non è possibile modificare prodotti e composizioni se è presente una prenotazione collegata alla tessera");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
cancellaComposizioni();
|
||||
MyTableModel model = (MyTableModel) tblListaProdotti.getModel();
|
||||
Integer riga = tblListaProdotti.getSelectedRow();
|
||||
@ -3164,8 +3334,7 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
}//GEN-LAST:event_btnImpostaClienteActionPerformed
|
||||
|
||||
private void jbtnAnnullaActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jbtnAnnullaActionPerformed
|
||||
if(idPrenotazione > 0)
|
||||
{
|
||||
if (idPrenotazione > 0) {
|
||||
Object selectedValue = JOptionPane.showOptionDialog(this, "Desideri bypassare la prenotazione?", "Attenzione",
|
||||
JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, siNoOptions, siNoOptions[0]);
|
||||
if (Integer.parseInt(selectedValue.toString()) == 0) {
|
||||
@ -3175,9 +3344,7 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
flagPrenotazionePagata = false;
|
||||
}
|
||||
//JOptionPane.showMessageDialog(this, "Non è possibile modificare prodotti e composizioni se è presente una prenotazione collegata alla tessera");
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
MyTableModel model = (MyTableModel) tblListaProdotti.getModel();
|
||||
if (model.getRowCount() > 0) {
|
||||
Object selectedValue = JOptionPane.showOptionDialog(this, "Desideri annullare tutte le righe?", "Attenzione",
|
||||
@ -3493,8 +3660,7 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
}
|
||||
//---------------------------
|
||||
lblResiduo.setText("€ " + df2.format(saldo - t));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
lblResiduo.setText("€ " + df2.format(saldo));
|
||||
}
|
||||
|
||||
@ -3623,7 +3789,6 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void chiudi() {
|
||||
|
||||
//04-02-2019---------------------------------------------
|
||||
@ -3664,8 +3829,7 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
//mostra messaggio ma non cancella i piatti
|
||||
JOptionPane.showMessageDialog(this, "Saldo insufficiente!", "Saldo", JOptionPane.OK_OPTION);
|
||||
return;
|
||||
@ -3810,7 +3974,8 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
spiaProgressivo = true;
|
||||
}
|
||||
|
||||
} /*else {
|
||||
}
|
||||
/*else {
|
||||
StringBuilder elencoIdProdotti = new StringBuilder();
|
||||
for (int i = model.getRowCount() - 1; i >= 0; i--) {
|
||||
Long idCateg = Long.valueOf(model.getValueAt(i, colCategoria).toString());
|
||||
@ -3911,9 +4076,10 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se i prodotti selezionati differiscono da quelli presenti
|
||||
* nella prenotazione collegata alla tessera. L'utente può scegliere di
|
||||
* modificare i prodotti, oppure di azzerare la lista
|
||||
* Verifica se i prodotti selezionati differiscono da quelli presenti nella
|
||||
* prenotazione collegata alla tessera. L'utente può scegliere di modificare
|
||||
* i prodotti, oppure di azzerare la lista
|
||||
*
|
||||
* @param elencoIdProdotto
|
||||
* @return
|
||||
*/
|
||||
@ -4269,8 +4435,6 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
jBtnAsportoActionPerformed(null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (abilitatoPassaggi != 0L) {
|
||||
controllaPrenotazioni();
|
||||
}
|
||||
@ -4462,7 +4626,9 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcolo dei prezzi dei prodotti sulla base del profilo tariffario corrente
|
||||
* Calcolo dei prezzi dei prodotti sulla base del profilo tariffario
|
||||
* corrente
|
||||
*
|
||||
* @param model
|
||||
*/
|
||||
private void forzaPrezzi(MyTableModel model) {
|
||||
@ -4847,8 +5013,9 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo che stampa un messaggio sul POS
|
||||
* Se il POS è chiuso, stampa a prescindere, altrimenti stampa solo se non è stata letta una tessera
|
||||
* Metodo che stampa un messaggio sul POS Se il POS è chiuso, stampa a
|
||||
* prescindere, altrimenti stampa solo se non è stata letta una tessera
|
||||
*
|
||||
* @param flagAperto
|
||||
*/
|
||||
private void stampaSuPos(boolean flagAperto) {
|
||||
@ -4862,8 +5029,7 @@ public class PuntoCassa extends JFrame implements ActionListener {
|
||||
if (flagAperto && jTxtTessera.getText().trim().length() <= 0) {
|
||||
String riga = Utils.spaziBianchi(messageDisplay, 20, true) + "POS " + idPuntoCassaString + " APERTO";
|
||||
display(riga, null);
|
||||
}
|
||||
else if (!flagAperto){
|
||||
} else if (!flagAperto) {
|
||||
String riga = Utils.spaziBianchi(messageDisplay, 20, true) + "POS " + idPuntoCassaString + " CHIUSO";
|
||||
display(riga, null);
|
||||
}
|
||||
|
51
src/puntocassa/utils/QRScanner.java
Normal file
51
src/puntocassa/utils/QRScanner.java
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
|
||||
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
|
||||
*/
|
||||
package puntocassa.utils;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author assis
|
||||
*/
|
||||
import com.github.sarxos.webcam.Webcam;
|
||||
import com.google.zxing.*;
|
||||
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
|
||||
import com.google.zxing.common.HybridBinarizer;
|
||||
|
||||
import java.awt.Dimension;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class QRScanner {
|
||||
|
||||
public static void main(String[] args) {
|
||||
Webcam webcam = Webcam.getDefault();
|
||||
webcam.setViewSize(new Dimension(1280, 720));
|
||||
webcam.open();
|
||||
|
||||
while (true) {
|
||||
BufferedImage image = webcam.getImage();
|
||||
if (image == null) continue;
|
||||
|
||||
LuminanceSource source = new BufferedImageLuminanceSource(image);
|
||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
|
||||
try {
|
||||
List<String> dataResult = new ArrayList<>();
|
||||
Result result = new MultiFormatReader().decode(bitmap);
|
||||
System.out.println("QR Code trovato: " + result.getText());
|
||||
//decripting
|
||||
dataResult = QrCryptoService.decryptAndValidate(result.getText());
|
||||
System.out.println("QR code decriptato: " + dataResult);
|
||||
break;
|
||||
} catch (NotFoundException e) {
|
||||
// Nessun QR code trovato nel frame
|
||||
}
|
||||
}
|
||||
|
||||
webcam.close();
|
||||
}
|
||||
}
|
||||
|
179
src/puntocassa/utils/QrCryptoService.java
Normal file
179
src/puntocassa/utils/QrCryptoService.java
Normal file
@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
|
||||
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
|
||||
*/
|
||||
package puntocassa.utils;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author assis
|
||||
*/
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Duration;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.*;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class QrCryptoService {
|
||||
|
||||
private static final byte[] AES_KEY = hexToBytes("3031323334353637383961626364656630313233343536373839616263646566");
|
||||
private static final Duration CLOCK_SKEW = Duration.ofSeconds(60);
|
||||
|
||||
public static String createEncryptedQr(String uid, OffsetDateTime issuedAt, OffsetDateTime expiresAt) throws GeneralSecurityException, JSONException {
|
||||
JSONObject payload = new JSONObject()
|
||||
.put("uid", uid)
|
||||
.put("issued_at", issuedAt.toString())
|
||||
.put("expires_at", expiresAt.toString());
|
||||
|
||||
byte[] iv = SecureRandom.getInstanceStrong().generateSeed(16);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(AES_KEY, "AES"), new IvParameterSpec(iv));
|
||||
byte[] encrypted = cipher.doFinal(payload.toString().getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// Concatenazione IV + encrypted
|
||||
ByteBuffer buffer = ByteBuffer.allocate(iv.length + encrypted.length);
|
||||
buffer.put(iv);
|
||||
buffer.put(encrypted);
|
||||
byte[] combined = buffer.array();
|
||||
|
||||
// Codifica tutto in Base64
|
||||
return Base64.getEncoder().encodeToString(combined);
|
||||
}
|
||||
|
||||
|
||||
public static List<String> decryptAndValidate(String base64Qr) {
|
||||
List<String> resultData = new ArrayList<>();
|
||||
try {
|
||||
// Step 1: Decodifica base64 del QR
|
||||
byte[] decoded = Base64.getDecoder().decode(base64Qr);
|
||||
String jsonString = new String(decoded, StandardCharsets.UTF_8);
|
||||
|
||||
// Step 2: Parsing JSON
|
||||
JSONObject qrObject = new JSONObject(jsonString);
|
||||
String ivBase64 = qrObject.getString("iv");
|
||||
String dataBase64 = qrObject.getString("data");
|
||||
|
||||
// Step 3: Decodifica separata di IV e data
|
||||
byte[] iv = Base64.getDecoder().decode(ivBase64);
|
||||
byte[] ciphertext = Base64.getDecoder().decode(dataBase64);
|
||||
|
||||
// Step 4: Decrittazione AES
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(AES_KEY, "AES"), new IvParameterSpec(iv));
|
||||
byte[] decrypted = cipher.doFinal(ciphertext);
|
||||
|
||||
// Step 5: Parsing del payload decifrato
|
||||
JSONObject payload = new JSONObject(new String(decrypted, StandardCharsets.UTF_8));
|
||||
ValidationResult result = validatePayload(payload);
|
||||
|
||||
if (result.valid()) {
|
||||
resultData.add("VALID");
|
||||
resultData.add(result.uid());
|
||||
} else {
|
||||
resultData.add("NOT VALID");
|
||||
resultData.add("Errore validazione QR code: " + result.reason());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
resultData.add("NOT VALID");
|
||||
resultData.add("Errore di decifratura o struttura non valida. QR letto: " + base64Qr);
|
||||
}
|
||||
|
||||
return resultData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static ValidationResult validatePayload(JSONObject payload) {
|
||||
try {
|
||||
String uid = payload.getString("uid");
|
||||
|
||||
//con timezone esplicito
|
||||
OffsetDateTime issuedAt = OffsetDateTime.parse(payload.getString("issued_at"));
|
||||
OffsetDateTime expiresAt = OffsetDateTime.parse(payload.getString("expires_at"));
|
||||
Instant now = Instant.now();
|
||||
Instant iat = issuedAt.toInstant();
|
||||
Instant exp = expiresAt.toInstant();
|
||||
|
||||
if (!uid.matches("[A-Z0-9]{16}"))
|
||||
return ValidationResult.invalid("Formato UID non valido");
|
||||
if (exp.isBefore(iat))
|
||||
return ValidationResult.invalid("Scadenza antecedente all'emissione");
|
||||
if (now.plus(CLOCK_SKEW).isBefore(iat))
|
||||
return ValidationResult.invalid("QR non ancora valido");
|
||||
if (now.minus(CLOCK_SKEW).isAfter(exp))
|
||||
return ValidationResult.invalid("QR scaduto");
|
||||
|
||||
//senza timezone esplicito
|
||||
/*LocalDateTime issuedAt = LocalDateTime.parse(payload.getString("issued_at"));
|
||||
LocalDateTime expiresAt = LocalDateTime.parse(payload.getString("expires_at"));
|
||||
|
||||
ZoneId zone = ZoneId.systemDefault(); // oppure un valore fisso come ZoneId.of("Europe/Rome")
|
||||
|
||||
Instant now = Instant.now();
|
||||
Instant iat = issuedAt.atZone(zone).toInstant();
|
||||
Instant exp = expiresAt.atZone(zone).toInstant();
|
||||
|
||||
if (!uid.matches("[A-Z0-9]{16}"))
|
||||
return ValidationResult.invalid("Formato UID non valido");
|
||||
|
||||
if (exp.isBefore(iat))
|
||||
return ValidationResult.invalid("Scadenza antecedente all'emissione");
|
||||
|
||||
if (now.plus(CLOCK_SKEW).isBefore(iat))
|
||||
return ValidationResult.invalid("QR non ancora valido");
|
||||
|
||||
if (now.minus(CLOCK_SKEW).isAfter(exp))
|
||||
return ValidationResult.invalid("QR scaduto");*/
|
||||
|
||||
|
||||
return ValidationResult.valid(uid, iat, exp);
|
||||
|
||||
} catch (Exception e) {
|
||||
return ValidationResult.invalid("Errore di validazione QR Code: Payload malformato o incompleto");
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] hexToBytes(String hex) {
|
||||
return new BigInteger(hex, 16).toByteArray().length == 33
|
||||
? Arrays.copyOfRange(new BigInteger(hex, 16).toByteArray(), 1, 33)
|
||||
: new BigInteger(hex, 16).toByteArray();
|
||||
}
|
||||
|
||||
public record ValidationResult(boolean valid, String reason, String uid, Instant issuedAt, Instant expiresAt) {
|
||||
public static ValidationResult valid(String uid, Instant issuedAt, Instant expiresAt) {
|
||||
return new ValidationResult(true, null, uid, issuedAt, expiresAt);
|
||||
}
|
||||
|
||||
public static ValidationResult invalid(String reason) {
|
||||
return new ValidationResult(false, reason, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
List<String> dataResult = new ArrayList<>();
|
||||
OffsetDateTime now = OffsetDateTime.now().withNano(0);
|
||||
OffsetDateTime expiration = now.plusSeconds(CLOCK_SKEW.getSeconds());
|
||||
|
||||
//String qr = createEncryptedQr("TSTTST91A48A271O", now, expiration);
|
||||
String qr = "eyJpdiI6Ik0xTVJvR3dMbm40UGFMalFEVUh5M1E9PSIsImRhdGEiOiJSVERTTkFURi8wK0lrekFGVVRaSkMrb1Y5QWVZcFRnMFFVKzgzY25QUVgrM2ZMVkRMV3kyOFRsMnBIQTlBRWVGTFQvK0pxSWNtblBYRnBjMys5T0tVWHJpc3FQc1VHazUxRzFpSERSOFJ1eEFCSWhPaTZlamlkdHU4d090WmRzY2F2Y0FRbmNESTd3bGxJNWgvOFFmZnc9PSJ9";
|
||||
System.out.println("QR generato:\n" + qr + "\n");
|
||||
|
||||
dataResult = decryptAndValidate(qr);
|
||||
System.out.println("Verifica:\n" + dataResult);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user