q = em.createQuery("SELECT COUNT(u) FROM UserEntity u", Long.class);
+ Long count = q.getSingleResult();
+ return count == 0;
+ }
+
+ public void populate() {
+ UserEntity u1 = new UserEntity(UUID.randomUUID().toString(),
+ "ricolinad", PasswordHasher.hash("ricardo2025"));
+ UserEntity u2 = new UserEntity(UUID.randomUUID().toString(),
+ "alvgulveg", PasswordHasher.hash("alcalde_dimision"));
+ UserEntity u3 = new UserEntity(UUID.randomUUID().toString(),
+ "smt4497", PasswordHasher.hash("quemenlaus"));
+ try {
+ em.getTransaction().begin();
+ em.persist(u1);
+ em.persist(u2);
+ em.persist(u3);
+ em.getTransaction().commit();
+ } catch (Exception e) {
+ em.getTransaction().rollback();
+ } finally {
+ em.close();
+ }
+ }
+}
diff --git a/BYODSEC/src/main/java/net/miarma/byodsec/common/db/dao/SessionDAO.java b/BYODSEC/src/main/java/net/miarma/byodsec/common/db/dao/SessionDAO.java
new file mode 100644
index 0000000..f01329e
--- /dev/null
+++ b/BYODSEC/src/main/java/net/miarma/byodsec/common/db/dao/SessionDAO.java
@@ -0,0 +1,20 @@
+package net.miarma.byodsec.common.db.dao;
+
+import jakarta.persistence.EntityManager;
+import net.miarma.byodsec.common.db.entities.SessionEntity;
+
+public class SessionDAO {
+ public SessionEntity getByUserId(EntityManager em, String userId) {
+ return em.createQuery("SELECT s FROM SessionEntity s WHERE s.userId = :userId", SessionEntity.class)
+ .setParameter("userId", userId)
+ .getSingleResult();
+ }
+
+ public boolean isLoggedIn(EntityManager em, String userId) {
+ Long count = em.createQuery(
+ "SELECT COUNT(s) FROM SessionEntity s WHERE s.userId = :userId", Long.class)
+ .setParameter("userId", userId)
+ .getSingleResult();
+ return count > 0;
+ }
+}
diff --git a/BYODSEC/src/main/java/net/miarma/byodsec/common/db/dao/UserDAO.java b/BYODSEC/src/main/java/net/miarma/byodsec/common/db/dao/UserDAO.java
new file mode 100644
index 0000000..92b3cad
--- /dev/null
+++ b/BYODSEC/src/main/java/net/miarma/byodsec/common/db/dao/UserDAO.java
@@ -0,0 +1,16 @@
+package net.miarma.byodsec.common.db.dao;
+
+import jakarta.persistence.EntityManager;
+import net.miarma.byodsec.common.db.entities.UserEntity;
+
+public class UserDAO {
+ public UserEntity getByUserName(EntityManager em, String userName) {
+ try {
+ return em.createQuery("SELECT u FROM UserEntity u WHERE u.userName = :userName", UserEntity.class)
+ .setParameter("userName", userName)
+ .getSingleResult();
+ } catch (jakarta.persistence.NoResultException e) {
+ return null; // No se encontró, devolvemos null
+ }
+ }
+}
diff --git a/BYODSEC/src/main/java/net/miarma/byodsec/common/db/dto/LoginDTO.java b/BYODSEC/src/main/java/net/miarma/byodsec/common/db/dto/LoginDTO.java
new file mode 100644
index 0000000..0af78d9
--- /dev/null
+++ b/BYODSEC/src/main/java/net/miarma/byodsec/common/db/dto/LoginDTO.java
@@ -0,0 +1,30 @@
+package net.miarma.byodsec.common.db.dto;
+
+public class LoginDTO {
+ private String username;
+ private String password;
+
+ public LoginDTO() {
+ }
+
+ public LoginDTO(String username, String password) {
+ this.username = username;
+ this.password = password;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+}
diff --git a/BYODSEC/src/main/java/net/miarma/byodsec/common/db/dto/MessageDTO.java b/BYODSEC/src/main/java/net/miarma/byodsec/common/db/dto/MessageDTO.java
new file mode 100644
index 0000000..020c549
--- /dev/null
+++ b/BYODSEC/src/main/java/net/miarma/byodsec/common/db/dto/MessageDTO.java
@@ -0,0 +1,31 @@
+package net.miarma.byodsec.common.db.dto;
+
+public class MessageDTO {
+ private String fromUserName;
+ private String messageText;
+
+ public MessageDTO() {}
+
+ public MessageDTO(String fromUserName, String messageText) {
+ this.fromUserName = fromUserName;
+ this.messageText = messageText;
+ }
+
+ public String getFromUserName() {
+ return fromUserName;
+ }
+
+ public void setFromUserName(String fromUserName) {
+ this.fromUserName = fromUserName;
+ }
+
+ public String getMessageText() {
+ return messageText;
+ }
+
+ public void setMessageText(String messageText) {
+ this.messageText = messageText;
+ }
+
+
+}
\ No newline at end of file
diff --git a/BYODSEC/src/main/java/net/miarma/byodsec/common/db/entities/SessionEntity.java b/BYODSEC/src/main/java/net/miarma/byodsec/common/db/entities/SessionEntity.java
new file mode 100644
index 0000000..e4333de
--- /dev/null
+++ b/BYODSEC/src/main/java/net/miarma/byodsec/common/db/entities/SessionEntity.java
@@ -0,0 +1,66 @@
+package net.miarma.byodsec.common.db.entities;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "sessions")
+public class SessionEntity {
+ @Id
+ @Column(name = "sessionId", nullable = false, updatable = false)
+ private String sessionId;
+
+ @Column(name = "userId", nullable = false)
+ private String userId;
+
+ @Column(name = "createdAt", nullable = false, updatable = false, insertable = false)
+ private LocalDateTime createdAt;
+
+ @Column(name = "expiresAt", nullable = false, updatable = false, insertable = false)
+ private LocalDateTime expiresAt;
+
+ public SessionEntity() {}
+
+ public SessionEntity(String sessionId, String userId, LocalDateTime createdAt, LocalDateTime expiresAt) {
+ this.sessionId = sessionId;
+ this.userId = userId;
+ this.createdAt = createdAt;
+ this.expiresAt = expiresAt;
+ }
+
+ public String getSessionId() {
+ return sessionId;
+ }
+
+ public void setSessionId(String sessionId) {
+ this.sessionId = sessionId;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public LocalDateTime getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(LocalDateTime createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public LocalDateTime getExpiresAt() {
+ return expiresAt;
+ }
+
+ public void setExpiresAt(LocalDateTime expiresAt) {
+ this.expiresAt = expiresAt;
+ }
+}
\ No newline at end of file
diff --git a/BYODSEC/src/main/java/net/miarma/byodsec/common/db/entities/UserEntity.java b/BYODSEC/src/main/java/net/miarma/byodsec/common/db/entities/UserEntity.java
new file mode 100644
index 0000000..ee04c6c
--- /dev/null
+++ b/BYODSEC/src/main/java/net/miarma/byodsec/common/db/entities/UserEntity.java
@@ -0,0 +1,52 @@
+package net.miarma.byodsec.common.db.entities;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+@Entity
+@Table(name = "users")
+public class UserEntity {
+ @Id
+ @Column(name = "userId", nullable = false, updatable = false, length = 36)
+ private String userId;
+
+ @Column(name = "userName", unique = true, nullable = false, length = 64)
+ private String userName;
+
+ @Column(name = "password", nullable = false, length = 256)
+ private String password;
+
+ public UserEntity() {}
+
+ public UserEntity(String userId, String userName, String password) {
+ this.userId = userId;
+ this.userName = userName;
+ this.password = password;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public void setUserName(String userName) {
+ this.userName = userName;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+}
\ No newline at end of file
diff --git a/BYODSEC/src/main/java/net/miarma/byodsec/common/security/IntegrityProvider.java b/BYODSEC/src/main/java/net/miarma/byodsec/common/security/IntegrityProvider.java
new file mode 100644
index 0000000..93b6b74
--- /dev/null
+++ b/BYODSEC/src/main/java/net/miarma/byodsec/common/security/IntegrityProvider.java
@@ -0,0 +1,30 @@
+package net.miarma.byodsec.common.security;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.Base64;
+
+public class IntegrityProvider {
+ public static String generateHMAC(String key, String data) throws Exception {
+ Mac sha256HMAC = Mac.getInstance("HmacSHA256");
+ SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
+ sha256HMAC.init(secretKey);
+ return byteArrayToHex(sha256HMAC.doFinal(data.getBytes(StandardCharsets.UTF_8)));
+ }
+
+ public static String generateNonce() {
+ byte[] nonce = new byte[16];
+ new SecureRandom().nextBytes(nonce);
+ return Base64.getEncoder().encodeToString(nonce);
+ }
+
+ public static String byteArrayToHex(byte[] arr) {
+ StringBuilder sb = new StringBuilder(arr.length * 2);
+ for(byte b : arr) {
+ sb.append(String.format("%02x", b));
+ }
+ return sb.toString();
+ }
+}
diff --git a/BYODSEC/src/main/java/net/miarma/byodsec/common/security/PasswordHasher.java b/BYODSEC/src/main/java/net/miarma/byodsec/common/security/PasswordHasher.java
new file mode 100644
index 0000000..6349d63
--- /dev/null
+++ b/BYODSEC/src/main/java/net/miarma/byodsec/common/security/PasswordHasher.java
@@ -0,0 +1,22 @@
+package net.miarma.byodsec.common.security;
+
+import org.mindrot.jbcrypt.BCrypt;
+
+/**
+ * Clase de utilidad para el hash de contraseñas.
+ * Utiliza BCrypt para generar y verificar hashes de contraseñas.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class PasswordHasher {
+
+ private static final int SALT_ROUNDS = 12;
+
+ public static String hash(String plainPassword) {
+ return BCrypt.hashpw(plainPassword, BCrypt.gensalt(SALT_ROUNDS));
+ }
+
+ public static boolean verify(String plainPassword, String hashedPassword) {
+ return BCrypt.checkpw(plainPassword, hashedPassword);
+ }
+}
\ No newline at end of file
diff --git a/BYODSEC/src/main/java/net/miarma/byodsec/common/socket/SocketRequest.java b/BYODSEC/src/main/java/net/miarma/byodsec/common/socket/SocketRequest.java
new file mode 100644
index 0000000..5209d1a
--- /dev/null
+++ b/BYODSEC/src/main/java/net/miarma/byodsec/common/socket/SocketRequest.java
@@ -0,0 +1,41 @@
+package net.miarma.byodsec.common.socket;
+
+public class SocketRequest {
+ private SocketRequest.Action action;
+ private Object payload;
+
+ public SocketRequest() {}
+
+ public SocketRequest(SocketRequest.Action action, Object payload) {
+ this.action = action;
+ this.payload = payload;
+ }
+
+ public SocketRequest.Action getAction() {
+ return action;
+ }
+
+ public void setAction(SocketRequest.Action action) {
+ this.action = action;
+ }
+
+ public Object getPayload() {
+ return payload;
+ }
+
+ public void setPayload(Object payload) {
+ this.payload = payload;
+ }
+
+ @Override
+ public String toString() {
+ return "SocketRequest{" +
+ "action='" + action.name() + '\'' +
+ ", payload=" + (payload != null ? payload.toString() : "null") +
+ '}';
+ }
+
+ public enum Action {
+ LOGIN, REGISTER, SEND_MESSAGE, LOGOUT
+ }
+}
diff --git a/BYODSEC/src/main/java/net/miarma/byodsec/common/socket/SocketResponse.java b/BYODSEC/src/main/java/net/miarma/byodsec/common/socket/SocketResponse.java
new file mode 100644
index 0000000..80e77f1
--- /dev/null
+++ b/BYODSEC/src/main/java/net/miarma/byodsec/common/socket/SocketResponse.java
@@ -0,0 +1,67 @@
+package net.miarma.byodsec.common.socket;
+
+public class SocketResponse {
+
+ private SocketStatus status;
+ private Message message;
+ private Object data;
+
+ public SocketResponse(SocketStatus status, Message message) {
+ this(status, message, message != null ? message.getMessage() : null);
+ }
+
+ public SocketResponse(SocketStatus status, Message message, Object data) {
+ this.status = status;
+ this.message = message;
+ this.data = data;
+ }
+
+ public SocketStatus getStatus() {
+ return status;
+ }
+
+ public Message getMessage() {
+ return message;
+ }
+
+ public Object getData() {
+ return data;
+ }
+
+ @Override
+ public String toString() {
+ return "SocketResponse{" +
+ "status=" + status +
+ ", message=" + (message != null ? message.name() : "null") +
+ ", data=" + (data != null ? data.toString() : "null") +
+ '}';
+ }
+
+ public enum Message {
+ LOGGED_OUT("Ha cerrado sesión correctamente"),
+ LOGGED_IN("Ha iniciado sesión correctamente"),
+ MESSAGE_SENT("Su mensaje se ha enviado"),
+ USER_NOT_FOUND("El usuario no existe."),
+ WRONG_PASSWORD("Contraseña incorrecta."),
+ USER_ALREADY_EXISTS("El usuario ya está registrado."),
+ CONNECTION_ERROR("Error de conexión con el servidor."),
+ UNKNOWN_ERROR("Ha ocurrido un error inesperado."),
+ SESSION_EXPIRED("Tu sesión ha expirado."),
+ INTEGRITY_FAIL("Fallo de integridad en el mensaje."),
+ INVALID_AMOUNT("Cantidad no válida."),
+ REPLAY_ATTACK("Intento de repetición detectado."),
+ RECIPIENT_NOT_FOUND("El destinatario no existe"),
+ REGISTERED("Se ha registrado correctamente"),
+ SESSION_ACTIVE("Ya hay una sesión activa para este usuario.");
+
+ private final String message;
+
+ Message(String userMessage) {
+ this.message = userMessage;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+ }
+}
diff --git a/BYODSEC/src/main/java/net/miarma/byodsec/common/socket/SocketStatus.java b/BYODSEC/src/main/java/net/miarma/byodsec/common/socket/SocketStatus.java
new file mode 100644
index 0000000..09b987a
--- /dev/null
+++ b/BYODSEC/src/main/java/net/miarma/byodsec/common/socket/SocketStatus.java
@@ -0,0 +1,15 @@
+package net.miarma.byodsec.common.socket;
+
+public enum SocketStatus {
+ OK,
+ ERROR,
+ SESSION_EXPIRED,
+ ALREADY_LOGGED_IN,
+ INTEGRITY_FAIL,
+ UNAUTHORIZED,
+ INVALID_REQUEST;
+
+ public static boolean isOk(String status) {
+ return OK.name().equalsIgnoreCase(status);
+ }
+}
diff --git a/BYODSEC/src/main/java/net/miarma/byodsec/server/ClientHandler.java b/BYODSEC/src/main/java/net/miarma/byodsec/server/ClientHandler.java
new file mode 100644
index 0000000..348a3e7
--- /dev/null
+++ b/BYODSEC/src/main/java/net/miarma/byodsec/server/ClientHandler.java
@@ -0,0 +1,156 @@
+package net.miarma.byodsec.server;
+
+import com.google.gson.Gson;
+import jakarta.persistence.EntityManager;
+import net.miarma.byodsec.common.db.DBConnector;
+import net.miarma.byodsec.common.db.dao.SessionDAO;
+import net.miarma.byodsec.common.db.dao.UserDAO;
+import net.miarma.byodsec.common.db.dto.LoginDTO;
+import net.miarma.byodsec.common.db.dto.MessageDTO;
+import net.miarma.byodsec.common.db.entities.SessionEntity;
+import net.miarma.byodsec.common.db.entities.UserEntity;
+import net.miarma.byodsec.common.security.PasswordHasher;
+import net.miarma.byodsec.common.socket.SocketRequest;
+import net.miarma.byodsec.common.socket.SocketResponse;
+import net.miarma.byodsec.common.socket.SocketStatus;
+import org.slf4j.Logger;
+
+import javax.net.ssl.SSLSocket;
+import java.io.*;
+import java.util.UUID;
+
+public class ClientHandler implements Runnable {
+ private final SSLSocket socket;
+ private final Gson gson;
+ private final UserDAO userDAO;
+ private final SessionDAO sessionDAO;
+ private final DBConnector conn;
+ private final Logger logger;
+
+ public ClientHandler(SSLSocket socket, Gson gson, UserDAO userDAO, SessionDAO sessionDAO, DBConnector conn, Logger logger) {
+ this.socket = socket;
+ this.gson = gson;
+ this.userDAO = userDAO;
+ this.sessionDAO = sessionDAO;
+ this.conn = conn;
+ this.logger = logger;
+ }
+
+ @Override
+ public void run() {
+ try (
+ BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+ PrintWriter output = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true);
+ ) {
+ logger.info("Nueva conexión desde {}:{}", socket.getInetAddress(), socket.getPort());
+ SocketRequest msg = gson.fromJson(input.readLine(), SocketRequest.class);
+ SocketResponse res;
+
+ switch (msg.getAction()) {
+ case REGISTER -> {
+ LoginDTO dto = gson.fromJson(gson.toJson(msg.getPayload()), LoginDTO.class);
+ logger.info("Procesando REGISTER para {}", dto.getUsername());
+ EntityManager em = conn.getEntityManager();
+ try {
+ if (userDAO.getByUserName(em, dto.getUsername()) != null) {
+ res = new SocketResponse(SocketStatus.INVALID_REQUEST, SocketResponse.Message.USER_ALREADY_EXISTS);
+ } else {
+ em.getTransaction().begin();
+ UserEntity u = new UserEntity(UUID.randomUUID().toString(),
+ dto.getUsername(), PasswordHasher.hash(dto.getPassword()));
+ em.persist(u);
+ em.getTransaction().commit();
+ res = new SocketResponse(SocketStatus.OK, SocketResponse.Message.REGISTERED, u);
+ }
+ } catch (Exception e) {
+ if (em.getTransaction().isActive()) em.getTransaction().rollback();
+ logger.error(e.getMessage(), e);
+ res = new SocketResponse(SocketStatus.ERROR, SocketResponse.Message.UNKNOWN_ERROR, e.getMessage());
+ } finally {
+ conn.close();
+ }
+ }
+
+ case LOGIN -> {
+ LoginDTO dto = gson.fromJson(gson.toJson(msg.getPayload()), LoginDTO.class);
+ logger.info("Procesando LOGIN para {}", dto.getUsername());
+ try (EntityManager em = conn.getEntityManager()) {
+ UserEntity user = userDAO.getByUserName(em, dto.getUsername());
+ if (user == null) {
+ res = new SocketResponse(SocketStatus.INVALID_REQUEST, SocketResponse.Message.USER_NOT_FOUND);
+ } else if (PasswordHasher.verify(dto.getPassword(), user.getPassword())) {
+ if(sessionDAO.isLoggedIn(em, user.getUserId())) {
+ res = new SocketResponse(SocketStatus.ALREADY_LOGGED_IN, SocketResponse.Message.SESSION_ACTIVE);
+ } else {
+ res = new SocketResponse(SocketStatus.OK, SocketResponse.Message.LOGGED_IN, user);
+
+ SessionEntity session = new SessionEntity();
+ session.setSessionId(UUID.randomUUID().toString());
+ session.setUserId(user.getUserId());
+
+ em.getTransaction().begin();
+ em.persist(session);
+ em.getTransaction().commit();
+ }
+ } else {
+ res = new SocketResponse(SocketStatus.INVALID_REQUEST, SocketResponse.Message.WRONG_PASSWORD);
+ }
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ res = new SocketResponse(SocketStatus.ERROR, SocketResponse.Message.UNKNOWN_ERROR, e.getMessage());
+ }
+ }
+
+ case LOGOUT -> {
+ String userId = gson.fromJson(gson.toJson(msg.getPayload()), String.class);
+ logger.info("Procesando LOGOUT para {}", userId);
+ try (EntityManager em = conn.getEntityManager()) {
+ SessionEntity session = sessionDAO.getByUserId(em, userId);
+ if (session != null) {
+ em.getTransaction().begin();
+ em.remove(session);
+ em.getTransaction().commit();
+ res = new SocketResponse(SocketStatus.OK, SocketResponse.Message.LOGGED_OUT);
+ } else {
+ res = new SocketResponse(SocketStatus.SESSION_EXPIRED, SocketResponse.Message.SESSION_EXPIRED);
+ }
+ }
+ }
+
+ case SEND_MESSAGE -> {
+ MessageDTO dto = gson.fromJson(gson.toJson(msg.getPayload()), MessageDTO.class);
+
+ logger.info("Procesando SEND_MESSAGE de {} a Servidor | {}", dto.getFromUserName(), dto.getMessageText());
+
+ EntityManager em = conn.getEntityManager();
+ try {
+ UserEntity fromUser = userDAO.getByUserName(em, dto.getFromUserName());
+
+ if (fromUser == null || !sessionDAO.isLoggedIn(em, fromUser.getUserId())) {
+ res = new SocketResponse(SocketStatus.SESSION_EXPIRED, SocketResponse.Message.SESSION_EXPIRED);
+ } else if (dto.getMessageText().isBlank()) {
+ res = new SocketResponse(SocketStatus.INVALID_REQUEST, SocketResponse.Message.INVALID_AMOUNT);
+ } else {
+ res = new SocketResponse(SocketStatus.OK, SocketResponse.Message.MESSAGE_SENT, dto.getMessageText());
+ }
+
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ res = new SocketResponse(SocketStatus.ERROR, SocketResponse.Message.UNKNOWN_ERROR, e.getMessage());
+ } finally {
+ conn.close();
+ }
+ }
+
+ default -> {
+ logger.warn("Acción desconocida recibida: {}", msg.getAction());
+ res = new SocketResponse(SocketStatus.INVALID_REQUEST, SocketResponse.Message.UNKNOWN_ERROR);
+ }
+ }
+
+ output.println(gson.toJson(res));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/BYODSEC/src/main/java/net/miarma/byodsec/server/RemoteSocket.java b/BYODSEC/src/main/java/net/miarma/byodsec/server/RemoteSocket.java
new file mode 100644
index 0000000..7aeba2e
--- /dev/null
+++ b/BYODSEC/src/main/java/net/miarma/byodsec/server/RemoteSocket.java
@@ -0,0 +1,55 @@
+package net.miarma.byodsec.server;
+
+import java.io.IOException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLServerSocketFactory;
+import javax.net.ssl.SSLSocket;
+
+import net.miarma.byodsec.common.ConfigManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+import net.miarma.byodsec.common.db.DBConnector;
+import net.miarma.byodsec.common.db.dao.SessionDAO;
+import net.miarma.byodsec.common.db.dao.UserDAO;
+
+public class RemoteSocket {
+ static void main(String[] args) throws IOException {
+ ConfigManager configManager = ConfigManager.getInstance();
+ configManager.loadConfig();
+ DBConnector conn = DBConnector.getInstance();
+ UserDAO userDAO = new UserDAO();
+ SessionDAO sessionDAO = new SessionDAO();
+ Gson gson = new Gson();
+ final Logger logger = LoggerFactory.getLogger(RemoteSocket.class);
+
+ // especificamos mediante propiedades el KS y la contraseña
+ System.setProperty("javax.net.ssl.keyStore", configManager.getJksFile().getAbsolutePath());
+ System.setProperty("javax.net.ssl.keyStorePassword", configManager.getStringProperty("jksPassword"));
+ System.setProperty("javax.net.ssl.trustStore", configManager.getJksFile().getAbsolutePath());
+ System.setProperty("javax.net.ssl.trustStorePassword", configManager.getStringProperty("jksPassword"));
+
+ // inicializamos el socket SSL, la versión de TLS y los Cipher Suites a utilizar
+ SSLServerSocketFactory factory = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
+ SSLServerSocket serverSocket = (SSLServerSocket) factory.createServerSocket(6969);
+ serverSocket.setEnabledProtocols(new String[]{ "TLSv1.3" });
+ serverSocket.setEnabledCipherSuites(new String[]{
+ "TLS_AES_256_GCM_SHA384",
+ "TLS_AES_128_GCM_SHA256",
+ "TLS_CHACHA20_POLY1305_SHA256"
+ });
+ serverSocket.setNeedClientAuth(true);
+
+ // main loop
+ Executor executor = Executors.newFixedThreadPool(300);
+ while (true) {
+ SSLSocket socket = (SSLSocket) serverSocket.accept();
+ executor.execute(new ClientHandler(socket, gson, userDAO, sessionDAO, conn, logger));
+ }
+ }
+}
diff --git a/BYODSEC/src/main/resources/META-INF/persistence.xml b/BYODSEC/src/main/resources/META-INF/persistence.xml
new file mode 100644
index 0000000..91471a2
--- /dev/null
+++ b/BYODSEC/src/main/resources/META-INF/persistence.xml
@@ -0,0 +1,29 @@
+
+
+
+
+ Persistence unit for PAI-2
+
+
+ net.miarma.byodsec.common.db.entities.UserEntity
+ net.miarma.byodsec.common.db.entities.SessionEntity
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/BYODSEC/src/main/resources/default.properties b/BYODSEC/src/main/resources/default.properties
new file mode 100644
index 0000000..70ad8e9
--- /dev/null
+++ b/BYODSEC/src/main/resources/default.properties
@@ -0,0 +1 @@
+jksPassword=
\ No newline at end of file
diff --git a/BYODSEC/src/main/resources/images/banco.png b/BYODSEC/src/main/resources/images/banco.png
new file mode 100644
index 0000000..ebf2e60
Binary files /dev/null and b/BYODSEC/src/main/resources/images/banco.png differ
diff --git a/BYODSEC/src/main/resources/images/logo.png b/BYODSEC/src/main/resources/images/logo.png
new file mode 100644
index 0000000..7e5637f
Binary files /dev/null and b/BYODSEC/src/main/resources/images/logo.png differ
diff --git a/BYODSEC/src/main/resources/logback.xml b/BYODSEC/src/main/resources/logback.xml
new file mode 100644
index 0000000..a96ee4e
--- /dev/null
+++ b/BYODSEC/src/main/resources/logback.xml
@@ -0,0 +1,53 @@
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n
+
+
+
+
+ log/client.log
+
+ %d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n
+
+
+ log/client-%d{yyyy-MM-dd}.log
+ 30
+ true
+
+
+
+
+ log/server.log
+
+ %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
+
+
+ log/server-%d{yyyy-MM-dd}.log
+ 30
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/BYODSEC/src/main/resources/scripts/db.sql b/BYODSEC/src/main/resources/scripts/db.sql
new file mode 100644
index 0000000..0a391b8
--- /dev/null
+++ b/BYODSEC/src/main/resources/scripts/db.sql
@@ -0,0 +1,27 @@
+USE pai2;
+
+-- Cleanup
+DROP TABLE IF EXISTS sessions;
+DROP TABLE IF EXISTS users;
+-- END Cleanup
+
+-- DDL
+CREATE TABLE users (
+ userId UUID NOT NULL PRIMARY KEY,
+ userName VARCHAR(64) NOT NULL UNIQUE,
+ password VARCHAR(256) NOT NULL
+);
+
+CREATE TABLE sessions (
+ sessionId UUID NOT NULL PRIMARY KEY,
+ userId UUID NOT NULL UNIQUE,
+ createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ expiresAt TIMESTAMP AS (createdAt + INTERVAL 15 MINUTE) STORED,
+ FOREIGN KEY (userId) REFERENCES users(userId) ON DELETE CASCADE
+);
+
+CREATE EVENT IF NOT EXISTS clear_expired_sessions
+ON SCHEDULE EVERY 1 MINUTE
+DO
+DELETE FROM sessions WHERE expiresAt <= NOW();
+-- END DDL
\ No newline at end of file
diff --git a/DevSecOps/.gitkeep b/DevSecOps/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Integridos/.gitignore b/Integridos/.gitignore
new file mode 100644
index 0000000..ddbe856
--- /dev/null
+++ b/Integridos/.gitignore
@@ -0,0 +1,2 @@
+.env
+target/
\ No newline at end of file
diff --git a/Integridos/log/client.log b/Integridos/log/client.log
new file mode 100644
index 0000000..77ee674
--- /dev/null
+++ b/Integridos/log/client.log
@@ -0,0 +1,15 @@
+2025-10-06 19:44:03 INFO n.m.integridos.client.ClientSocket - El usuario 'alvgulveg' ha iniciado sesión correctamente.
+2025-10-06 19:44:12 INFO n.m.integridos.client.ClientSocket - Transacción enviada: alvgulveg -> smt4497 | 50.0€
+2025-10-06 19:44:13 INFO n.m.integridos.client.ClientSocket - Usuario 'alvgulveg' ha cerrado sesión correctamente.
+2025-10-06 19:48:16 WARN n.m.integridos.client.ClientSocket - Intento de login fallido para 'noexiste': El usuario no existe.
+2025-10-06 19:48:53 INFO n.m.integridos.client.ClientSocket - El usuario 'smt4497' ha iniciado sesión correctamente.
+2025-10-06 19:49:14 INFO n.m.integridos.client.ClientSocket - Transacción enviada: smt4497 -> alvgulveg | 50.0€
+2025-10-06 19:49:14 INFO n.m.integridos.client.ClientSocket - Usuario 'smt4497' ha cerrado sesión correctamente.
+2025-10-06 19:49:35 WARN n.m.integridos.client.ClientSocket - Intento de login fallido para 'smt4497': Contraseña incorrecta.
+2025-10-06 19:57:25 INFO n.m.integridos.client.ClientSocket - El usuario 'smt4497' ha iniciado sesión correctamente.
+2025-10-06 19:57:31 WARN n.m.integridos.client.ClientSocket - Error enviando transacción de smt4497 a alvgulveg: Cantidad no válida.
+2025-10-06 19:57:38 WARN n.m.integridos.client.ClientSocket - Error enviando transacción de smt4497 a alvgulveg: Cantidad no válida.
+2025-10-06 19:57:42 WARN n.m.integridos.client.ClientSocket - Error enviando transacción de smt4497 a noexiste: El destinatario no existe
+2025-10-06 19:57:52 INFO n.m.integridos.client.ClientSocket - Usuario 'smt4497' ha cerrado sesión correctamente.
+2025-10-06 19:57:58 WARN n.m.integridos.client.ClientSocket - Intento de login fallido para 'alvgulveg': Contraseña incorrecta.
+2025-10-06 19:58:04 WARN n.m.integridos.client.ClientSocket - Intento de login fallido para 'noexiste': El usuario no existe.
diff --git a/Integridos/log/server.log b/Integridos/log/server.log
new file mode 100644
index 0000000..cba59da
--- /dev/null
+++ b/Integridos/log/server.log
@@ -0,0 +1,46 @@
+2025-10-06 19:44:02 [main] INFO n.m.integridos.server.RemoteSocket - Nueva conexión desde /127.0.0.1:58622
+2025-10-06 19:44:02 [main] INFO n.m.integridos.server.RemoteSocket - Procesando LOGIN para alvgulveg
+2025-10-06 19:44:11 [main] INFO n.m.integridos.server.RemoteSocket - Nueva conexión desde /127.0.0.1:36988
+2025-10-06 19:44:11 [main] INFO n.m.integridos.server.RemoteSocket - Procesando SEND_TRANSACTION de alvgulveg a smt4497 | 50.0
+2025-10-06 19:44:13 [main] INFO n.m.integridos.server.RemoteSocket - Nueva conexión desde /127.0.0.1:36992
+2025-10-06 19:44:13 [main] INFO n.m.integridos.server.RemoteSocket - Procesando LOGOUT para 5b3532d0-d6ff-4845-8aa0-03a1d11a94b2
+2025-10-06 19:48:16 [main] INFO n.m.integridos.server.RemoteSocket - Nueva conexión desde /127.0.0.1:34878
+2025-10-06 19:48:16 [main] INFO n.m.integridos.server.RemoteSocket - Procesando LOGIN para noexiste
+2025-10-06 19:48:53 [main] INFO n.m.integridos.server.RemoteSocket - Nueva conexión desde /127.0.0.1:34066
+2025-10-06 19:48:53 [main] INFO n.m.integridos.server.RemoteSocket - Procesando LOGIN para smt4497
+2025-10-06 19:49:13 [main] INFO n.m.integridos.server.RemoteSocket - Nueva conexión desde /127.0.0.1:44270
+2025-10-06 19:49:13 [main] INFO n.m.integridos.server.RemoteSocket - Procesando SEND_TRANSACTION de smt4497 a alvgulveg | 50.0
+2025-10-06 19:49:13 [main] DEBUG n.m.integridos.server.RemoteSocket - HMAC recibida: 7f00bda9f22b98fdfbd0f2c1b61b4d40cd0eca66a38efb1304adf3fd33599fe0
+2025-10-06 19:49:13 [main] DEBUG n.m.integridos.server.RemoteSocket - Mensaje JSON para calcular HMAC: {"action":"SEND_TRANSACTION","payload":{"fromUserName":"smt4497","toUserName":"alvgulveg","amount":50.0,"nonce":"x8Dsu3aNocd014/bH7lymw\u003d\u003d"}}
+2025-10-06 19:49:13 [main] DEBUG n.m.integridos.server.RemoteSocket - HMAC generada: 7f00bda9f22b98fdfbd0f2c1b61b4d40cd0eca66a38efb1304adf3fd33599fe0
+2025-10-06 19:49:13 [main] DEBUG n.m.integridos.server.RemoteSocket - Las HMAC coinciden
+2025-10-06 19:49:14 [main] INFO n.m.integridos.server.RemoteSocket - Nueva conexión desde /127.0.0.1:44274
+2025-10-06 19:49:14 [main] INFO n.m.integridos.server.RemoteSocket - Procesando LOGOUT para 06a85107-285d-4b10-a412-427e63afecfb
+2025-10-06 19:49:35 [main] INFO n.m.integridos.server.RemoteSocket - Nueva conexión desde /127.0.0.1:59576
+2025-10-06 19:49:35 [main] INFO n.m.integridos.server.RemoteSocket - Procesando LOGIN para smt4497
+2025-10-06 19:57:25 [main] INFO n.m.integridos.server.RemoteSocket - Nueva conexión desde /127.0.0.1:55456
+2025-10-06 19:57:25 [main] INFO n.m.integridos.server.RemoteSocket - Procesando LOGIN para smt4497
+2025-10-06 19:57:31 [main] INFO n.m.integridos.server.RemoteSocket - Nueva conexión desde /127.0.0.1:41488
+2025-10-06 19:57:31 [main] INFO n.m.integridos.server.RemoteSocket - Procesando SEND_TRANSACTION de smt4497 a alvgulveg | -10.0
+2025-10-06 19:57:31 [main] DEBUG n.m.integridos.server.RemoteSocket - HMAC recibida: 25417b475e6cf28707e519bb6d49a1241d502de90ef5db3c2ab384fcabdb6bdf
+2025-10-06 19:57:31 [main] DEBUG n.m.integridos.server.RemoteSocket - Mensaje JSON para calcular HMAC: {"action":"SEND_TRANSACTION","payload":{"fromUserName":"smt4497","toUserName":"alvgulveg","amount":-10.0,"nonce":"Z5vRIKxPXsBBV6LKKZZITg\u003d\u003d"}}
+2025-10-06 19:57:31 [main] DEBUG n.m.integridos.server.RemoteSocket - HMAC generada: 25417b475e6cf28707e519bb6d49a1241d502de90ef5db3c2ab384fcabdb6bdf
+2025-10-06 19:57:31 [main] DEBUG n.m.integridos.server.RemoteSocket - Las HMAC coinciden
+2025-10-06 19:57:37 [main] INFO n.m.integridos.server.RemoteSocket - Nueva conexión desde /127.0.0.1:50254
+2025-10-06 19:57:37 [main] INFO n.m.integridos.server.RemoteSocket - Procesando SEND_TRANSACTION de smt4497 a alvgulveg | 1000.0
+2025-10-06 19:57:37 [main] DEBUG n.m.integridos.server.RemoteSocket - HMAC recibida: 2e2ac0a1df12e8127bdb046a932cee65187c872fbc536161d0b982037142fc76
+2025-10-06 19:57:37 [main] DEBUG n.m.integridos.server.RemoteSocket - Mensaje JSON para calcular HMAC: {"action":"SEND_TRANSACTION","payload":{"fromUserName":"smt4497","toUserName":"alvgulveg","amount":1000.0,"nonce":"3dOKgYJ9fKOV4IdU/94Arg\u003d\u003d"}}
+2025-10-06 19:57:37 [main] DEBUG n.m.integridos.server.RemoteSocket - HMAC generada: 2e2ac0a1df12e8127bdb046a932cee65187c872fbc536161d0b982037142fc76
+2025-10-06 19:57:37 [main] DEBUG n.m.integridos.server.RemoteSocket - Las HMAC coinciden
+2025-10-06 19:57:42 [main] INFO n.m.integridos.server.RemoteSocket - Nueva conexión desde /127.0.0.1:50270
+2025-10-06 19:57:42 [main] INFO n.m.integridos.server.RemoteSocket - Procesando SEND_TRANSACTION de smt4497 a noexiste | 10.0
+2025-10-06 19:57:42 [main] DEBUG n.m.integridos.server.RemoteSocket - HMAC recibida: dea4a26a0fee02ae093acff80eca84d6409918b58dcc0e1706f8c5bf77bdaa31
+2025-10-06 19:57:42 [main] DEBUG n.m.integridos.server.RemoteSocket - Mensaje JSON para calcular HMAC: {"action":"SEND_TRANSACTION","payload":{"fromUserName":"smt4497","toUserName":"noexiste","amount":10.0,"nonce":"ok94L3xsMJG/626yvUlyRQ\u003d\u003d"}}
+2025-10-06 19:57:42 [main] DEBUG n.m.integridos.server.RemoteSocket - HMAC generada: dea4a26a0fee02ae093acff80eca84d6409918b58dcc0e1706f8c5bf77bdaa31
+2025-10-06 19:57:42 [main] DEBUG n.m.integridos.server.RemoteSocket - Las HMAC coinciden
+2025-10-06 19:57:51 [main] INFO n.m.integridos.server.RemoteSocket - Nueva conexión desde /127.0.0.1:53674
+2025-10-06 19:57:51 [main] INFO n.m.integridos.server.RemoteSocket - Procesando LOGOUT para 06a85107-285d-4b10-a412-427e63afecfb
+2025-10-06 19:57:58 [main] INFO n.m.integridos.server.RemoteSocket - Nueva conexión desde /127.0.0.1:42006
+2025-10-06 19:57:58 [main] INFO n.m.integridos.server.RemoteSocket - Procesando LOGIN para alvgulveg
+2025-10-06 19:58:04 [main] INFO n.m.integridos.server.RemoteSocket - Nueva conexión desde /127.0.0.1:42008
+2025-10-06 19:58:04 [main] INFO n.m.integridos.server.RemoteSocket - Procesando LOGIN para noexiste
diff --git a/Integridos/pom.xml b/Integridos/pom.xml
new file mode 100644
index 0000000..73456ca
--- /dev/null
+++ b/Integridos/pom.xml
@@ -0,0 +1,155 @@
+
+ 4.0.0
+ net.miarma
+ integridos
+ 1.0.0
+ net.miarma.integridos.Integridos
+
+
+ 25
+ 25
+
+
+
+
+
+ com.google.code.gson
+ gson
+ 2.12.1
+
+
+
+
+
+ org.slf4j
+ slf4j-api
+ 2.0.12
+
+
+
+ ch.qos.logback
+ logback-classic
+ 1.5.13
+
+
+
+
+
+ org.mariadb.jdbc
+ mariadb-java-client
+ 3.5.6
+
+
+
+ jakarta.persistence
+ jakarta.persistence-api
+ 3.2.0
+
+
+
+ org.hibernate.orm
+ hibernate-core
+ 7.1.1.Final
+
+
+
+ com.zaxxer
+ HikariCP
+ 5.1.0
+
+
+
+
+
+ com.auth0
+ java-jwt
+ 4.5.0
+
+
+
+
+
+ org.mindrot
+ jbcrypt
+ 0.4
+
+
+
+
+
+ com.miglayout
+ miglayout-swing
+ 11.4.2
+
+
+
+ com.formdev
+ flatlaf-intellij-themes
+ 3.6.1
+
+
+
+ com.formdev
+ flatlaf
+ 3.6.1
+
+
+
+ com.formdev
+ flatlaf-extras
+ 3.6.1
+
+
+
+ com.github.jiconfont
+ jiconfont
+ 1.0.0
+
+
+
+ com.github.jiconfont
+ jiconfont-swing
+ 1.0.1
+
+
+
+ com.github.jiconfont
+ jiconfont-font_awesome
+ 4.7.0.1
+
+
+
+ com.github.jiconfont
+ jiconfont-google_material_design_icons
+ 2.2.0.2
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.3
+
+
+ package
+
+ shade
+
+
+ false
+
+
+ net.miarma.integridos.net.miarma.integridos.Integridos
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Integridos/src/main/java/module-info.java b/Integridos/src/main/java/module-info.java
new file mode 100644
index 0000000..f075056
--- /dev/null
+++ b/Integridos/src/main/java/module-info.java
@@ -0,0 +1,8 @@
+/**
+ *
+ */
+/**
+ *
+ */
+module BYODSEC {
+}
\ No newline at end of file
diff --git a/Integridos/src/main/java/net/miarma/integridos/Integridos.java b/Integridos/src/main/java/net/miarma/integridos/Integridos.java
new file mode 100644
index 0000000..11b9f35
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/Integridos.java
@@ -0,0 +1,78 @@
+package net.miarma.integridos;
+
+import com.formdev.flatlaf.themes.FlatMacLightLaf;
+import jiconfont.icons.font_awesome.FontAwesome;
+import jiconfont.icons.google_material_design_icons.GoogleMaterialDesignIcons;
+import jiconfont.swing.IconFontSwing;
+import net.miarma.integridos.common.ConfigManager;
+import net.miarma.integridos.common.Constants;
+import net.miarma.integridos.common.db.DBPopulator;
+import net.miarma.integridos.client.ui.MainWindow;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.*;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+
+public class Integridos {
+ private static final Logger logger = LoggerFactory.getLogger(Integridos.class);
+ private static ConfigManager configManager;
+
+ public static void main(String[] args) {
+ configManager = ConfigManager.getInstance();
+ createFiles();
+ configManager.loadConfig();
+ if(DBPopulator.getInstance().isFirstRun()) {
+ DBPopulator.getInstance().populate();
+ }
+ initUI();
+ }
+
+ private static void initUI() {
+ try {
+ UIManager.setLookAndFeel(new FlatMacLightLaf());
+ } catch(UnsupportedLookAndFeelException e) {
+ logger.error("Error setting LaF. Falling back to default Swing looks.");
+ }
+ IconFontSwing.register(FontAwesome.getIconFont());
+ IconFontSwing.register(GoogleMaterialDesignIcons.getIconFont());
+ SwingUtilities.invokeLater(() -> {
+ new MainWindow().setVisible(true);
+ });
+ }
+
+ private static void createFiles() {
+ File configFile = new File(configManager.getConfigFile().getAbsolutePath());
+ File parentDir = configFile.getParentFile();
+
+ if (!parentDir.exists()) {
+ if (parentDir.mkdirs()) {
+ logger.info("Created config directory: " + parentDir.getAbsolutePath());
+ } else {
+ logger.error("Failed to create config directory: " + parentDir.getAbsolutePath());
+ return;
+ }
+ }
+
+ if (!configFile.exists()) {
+ try (InputStream in = Integridos.class.getClassLoader().getResourceAsStream("default.properties")) {
+ if (in != null) {
+ Files.copy(in, configFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ logger.info("Default config file created at: " + configFile.getAbsolutePath());
+ } else {
+ logger.error("Resource default.properties not found!");
+ }
+ } catch (IOException e) {
+ logger.error("Error creating config file: ", e);
+ }
+ } else {
+ logger.info("Config file already exists at: " + configFile.getAbsolutePath());
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/Integridos/src/main/java/net/miarma/integridos/client/ClientSocket.java b/Integridos/src/main/java/net/miarma/integridos/client/ClientSocket.java
new file mode 100644
index 0000000..977a17a
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/client/ClientSocket.java
@@ -0,0 +1,63 @@
+package net.miarma.integridos.client;
+
+import com.google.gson.Gson;
+import net.miarma.integridos.common.*;
+import net.miarma.integridos.common.security.IntegrityProvider;
+import net.miarma.integridos.common.socket.SocketRequest;
+import net.miarma.integridos.common.socket.SocketResponse;
+import net.miarma.integridos.common.db.dto.LoginDTO;
+import net.miarma.integridos.common.db.dto.TransactionDTO;
+
+import java.io.*;
+import java.net.Socket;
+
+public class ClientSocket implements AutoCloseable {
+ private Socket socket;
+ private PrintWriter output;
+ private BufferedReader input;
+ private final Gson gson = new Gson();
+
+ public ClientSocket() throws IOException {
+ this.socket = new Socket("localhost", 6969);
+ this.output = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true);
+ this.input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+ }
+
+ public SocketResponse login(String username, String password) throws IOException {
+ SocketRequest msg = new SocketRequest(SocketRequest.Action.LOGIN, new LoginDTO(username, password));
+ output.println(gson.toJson(msg));
+ return gson.fromJson(input.readLine(), SocketResponse.class);
+ }
+
+ public SocketResponse register(String username, String password) throws IOException {
+ SocketRequest msg = new SocketRequest(SocketRequest.Action.REGISTER, new LoginDTO(username, password));
+ output.println(gson.toJson(msg));
+ return gson.fromJson(input.readLine(), SocketResponse.class);
+ }
+
+ public SocketResponse logout(String userId) throws IOException {
+ SocketRequest msg = new SocketRequest(SocketRequest.Action.LOGOUT, userId);
+ output.println(gson.toJson(msg));
+ return gson.fromJson(input.readLine(), SocketResponse.class);
+ }
+
+ public SocketResponse sendTransaction(TransactionDTO transaction) throws Exception {
+ transaction.setHmac(null);
+ SocketRequest msg = new SocketRequest(SocketRequest.Action.SEND_TRANSACTION, transaction);
+ String jsonMsg = gson.toJson(msg);
+ String hmac = IntegrityProvider.generateHMAC(ConfigManager.getInstance().getStringProperty("secret"), jsonMsg);
+ transaction.setHmac(hmac);
+ msg.setPayload(transaction);
+ output.println(gson.toJson(msg));
+ return gson.fromJson(input.readLine(), SocketResponse.class);
+ }
+
+ @Override
+ public void close() {
+ try {
+ input.close();
+ output.close();
+ socket.close();
+ } catch (Exception ignored) {}
+ }
+}
diff --git a/Integridos/src/main/java/net/miarma/integridos/client/ui/AutoShrinkLabel.java b/Integridos/src/main/java/net/miarma/integridos/client/ui/AutoShrinkLabel.java
new file mode 100644
index 0000000..7b3a3fc
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/client/ui/AutoShrinkLabel.java
@@ -0,0 +1,62 @@
+package net.miarma.integridos.client.ui;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class AutoShrinkLabel extends JLabel {
+ public AutoShrinkLabel(String text) {
+ super(text);
+ setHorizontalAlignment(SwingConstants.CENTER);
+ }
+
+ @Override
+ protected void paintComponent(Graphics g) {
+ String text = getText();
+ if (text == null || text.isEmpty()) {
+ super.paintComponent(g);
+ return;
+ }
+
+ Insets insets = getInsets();
+ int availableHeight = getHeight() - insets.top - insets.bottom;
+ int availableWidth = getWidth() - insets.left - insets.right;
+
+ if (availableWidth <= 0 || availableHeight <= 0) {
+ return;
+ }
+
+ Graphics2D graphics2d = (Graphics2D) g.create();
+
+ Font font = getFont();
+ int fontSize = font.getSize();
+
+ FontMetrics fm;
+ int textWidth;
+ int textHeight;
+
+ do {
+ Font testFont = font.deriveFont((float) fontSize);
+ fm = graphics2d.getFontMetrics(testFont);
+ textWidth = fm.stringWidth(text);
+ textHeight = fm.getHeight();
+ if (textWidth <= availableWidth && textHeight <= availableHeight) {
+ font = testFont;
+ break;
+ }
+ fontSize--;
+ } while (fontSize > 5);
+
+ graphics2d.setFont(font);
+
+ int x = (getWidth() - fm.stringWidth(text)) / 2;
+ int y = (getHeight() + fm.getAscent() - fm.getDescent()) / 2;
+
+ graphics2d.drawString(text, x, y);
+ graphics2d.dispose();
+ }
+
+ @Override
+ public Dimension getPreferredSize() {
+ return new Dimension(200, 50);
+ }
+}
diff --git a/Integridos/src/main/java/net/miarma/integridos/client/ui/MainWindow.java b/Integridos/src/main/java/net/miarma/integridos/client/ui/MainWindow.java
new file mode 100644
index 0000000..8baf548
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/client/ui/MainWindow.java
@@ -0,0 +1,492 @@
+/*
+ * Created by JFormDesigner on Wed Oct 01 16:21:10 CEST 2025
+ */
+
+package net.miarma.integridos.client.ui;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.IOException;
+import javax.swing.*;
+
+import com.google.gson.Gson;
+import jiconfont.icons.font_awesome.FontAwesome;
+import jiconfont.swing.IconFontSwing;
+import net.miarma.integridos.common.Constants;
+
+import net.miarma.integridos.common.security.IntegrityProvider;
+import net.miarma.integridos.common.socket.SocketResponse;
+import net.miarma.integridos.common.socket.SocketStatus;
+import net.miarma.integridos.common.db.dto.TransactionDTO;
+import net.miarma.integridos.common.db.entities.UserEntity;
+import net.miarma.integridos.client.ClientSocket;
+import net.miginfocom.swing.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author jomaa
+ */
+public class MainWindow extends JFrame {
+ public static UserEntity _loggedUser;
+ private AutoShrinkLabel balanceLabel = new AutoShrinkLabel("0.00€");
+ private static final Logger logger = LoggerFactory.getLogger(ClientSocket.class);
+
+ public MainWindow() {
+ initComponents();
+ this.setTitle(Constants.APP_NAME);
+ versionLabel.setText(Constants.APP_NAME + " v" + Constants.APP_VERSION + " by Security Team 33");
+ setIcons();
+ addBalanceLabel();
+ }
+
+ private void setIcons() {
+ userLabel.setIcon(IconFontSwing.buildIcon(FontAwesome.USER, 18, Color.BLACK));
+ passwordLabel.setIcon(IconFontSwing.buildIcon(FontAwesome.KEY, 18, Color.BLACK));
+ loginBtn.setIcon(IconFontSwing.buildIcon(FontAwesome.SIGN_IN, 18, Color.BLACK));
+ registerBtn.setIcon(IconFontSwing.buildIcon(FontAwesome.USER_PLUS, 18, Color.BLACK));
+ destLabel.setIcon(IconFontSwing.buildIcon(FontAwesome.USER_SECRET, 18, Color.BLACK));
+ amountLabel.setIcon(IconFontSwing.buildIcon(FontAwesome.MONEY, 18, Color.BLACK));
+ }
+
+ private void clearLoginInputs() {
+ userTextField.setText("");
+ passwordTextField.setText("");
+ }
+
+ private void clearMainInputs() {
+ destTextField.setText("");
+ amountTextField.setText("");
+ }
+
+ private void addBalanceLabel() {
+ balanceLabel.setFont(new Font("Adwaita Sans", Font.BOLD, 64));
+ balanceLabel.setHorizontalAlignment(SwingConstants.CENTER);
+ mainPanel.add(balanceLabel, "cell 0 0,alignx center,growx 0");
+ }
+
+ private void setView(String name) {
+ ((CardLayout)getContentPane().getLayout()).show(getContentPane(), name);
+ }
+
+ private void loginBtn(ActionEvent e) {
+ String username = userTextField.getText();
+ String password = new String(passwordTextField.getPassword());
+
+ try (ClientSocket client = new ClientSocket()) {
+ SocketResponse res = client.login(username, password);
+
+ if (res.getStatus() == SocketStatus.OK) {
+ _loggedUser = new Gson().fromJson(
+ new Gson().toJson(res.getData()),
+ UserEntity.class
+ );
+ logger.info("El usuario '{}' ha iniciado sesión correctamente.", username);
+ JOptionPane.showMessageDialog(this,
+ res.getMessage().getMessage(),
+ res.getStatus().toString(),
+ JOptionPane.INFORMATION_MESSAGE
+ );
+ setView("mainPanel");
+ balanceLabel.setText(String.format("%.2f€", _loggedUser.getBalance()));
+ } else {
+ logger.warn("Intento de login fallido para '{}': {}", username,
+ res.getMessage() != null ? res.getMessage().getMessage() : "Respuesta sin mensaje");
+ JOptionPane.showMessageDialog(this,
+ res.getMessage() != null ? res.getMessage().getMessage() : "Respuesta sin mensaje",
+ res.getStatus().toString(),
+ JOptionPane.ERROR_MESSAGE
+ );
+ }
+
+ clearLoginInputs();
+ } catch (IOException ex) {
+ logger.error("Error de conexión con el servidor al intentar loguear '{}': {}", username, ex.getMessage());
+ JOptionPane.showMessageDialog(this,
+ "Error de conexión con el servidor",
+ "Error",
+ JOptionPane.ERROR_MESSAGE
+ );
+ }
+ }
+
+ private void registerBtn(ActionEvent e) {
+ String username = userTextField.getText();
+ String password = new String(passwordTextField.getPassword());
+
+ try (ClientSocket client = new ClientSocket()) {
+ SocketResponse res = client.register(username, password);
+
+ if(res.getStatus() == SocketStatus.OK) {
+ logger.info("Usuario '{}' registrado correctamente.", username);
+ } else {
+ logger.warn("Error al registrar '{}': {}", username,
+ res.getMessage() != null ? res.getMessage().getMessage() : "Respuesta sin mensaje");
+ }
+
+ JOptionPane.showMessageDialog(this,
+ res.getMessage() != null ? res.getMessage().getMessage() : "Respuesta sin mensaje",
+ res.getStatus().toString(),
+ res.getStatus() == SocketStatus.OK ?
+ JOptionPane.INFORMATION_MESSAGE : JOptionPane.ERROR_MESSAGE
+ );
+
+ clearLoginInputs();
+ } catch (IOException ex) {
+ logger.error("Error de conexión con el servidor al registrar '{}': {}", username, ex.getMessage());
+ JOptionPane.showMessageDialog(this,
+ "Error de conexión con el servidor",
+ "Error",
+ JOptionPane.ERROR_MESSAGE
+ );
+ }
+ }
+
+ private void sendBtn(ActionEvent e) {
+ String destUser = destTextField.getText();
+ double amount;
+
+ try {
+ amount = Double.parseDouble(amountTextField.getText());
+ } catch (NumberFormatException ex) {
+ logger.warn("Cantidad no válida introducida: {}", amountTextField.getText());
+ JOptionPane.showMessageDialog(this,
+ "Cantidad no válida.",
+ "Error",
+ JOptionPane.ERROR_MESSAGE
+ );
+ return;
+ }
+
+ try (ClientSocket client = new ClientSocket()) {
+ String nonce = IntegrityProvider.generateNonce();
+ TransactionDTO dto = new TransactionDTO(_loggedUser.getUserName(), destUser, amount, nonce);
+ SocketResponse res = client.sendTransaction(dto);
+
+ JOptionPane.showMessageDialog(this,
+ res.getMessage() != null ? res.getMessage().getMessage() : "Respuesta sin mensaje",
+ res.getStatus().toString(),
+ res.getStatus() == SocketStatus.OK ?
+ JOptionPane.INFORMATION_MESSAGE : JOptionPane.ERROR_MESSAGE
+ );
+
+ if (res.getStatus() == SocketStatus.OK) {
+ logger.info("Transacción enviada: {} -> {} | {}€", _loggedUser.getUserName(), destUser, amount);
+ _loggedUser.setBalance(_loggedUser.getBalance() - amount);
+ balanceLabel.setText(String.format("%.2f€", _loggedUser.getBalance()));
+ } else if (res.getStatus() == SocketStatus.SESSION_EXPIRED) {
+ logger.warn("Sesión expirada para '{}'. Se cerrará sesión.", _loggedUser.getUserName());
+ _loggedUser = null;
+ setView("loginPanel");
+ clearMainInputs();
+ } else {
+ logger.warn("Error enviando transacción de {} a {}: {}", _loggedUser.getUserName(), destUser,
+ res.getMessage() != null ? res.getMessage().getMessage() : "Sin mensaje");
+ }
+
+ clearMainInputs();
+ } catch (Exception ex) {
+ logger.error("Error de conexión con el servidor enviando transacción de {} a {}: {}",
+ _loggedUser.getUserName(), destUser, ex.getMessage());
+ JOptionPane.showMessageDialog(this,
+ "Error de conexión con el servidor",
+ "Error",
+ JOptionPane.ERROR_MESSAGE
+ );
+ }
+ }
+
+ private void logout() {
+ if (_loggedUser != null) {
+ try (ClientSocket client = new ClientSocket()) {
+ SocketResponse res = client.logout(_loggedUser.getUserId());
+
+ if (res != null && res.getMessage() != null) {
+ if (res.getStatus() == SocketStatus.OK) {
+ logger.info("Usuario '{}' ha cerrado sesión correctamente.", _loggedUser.getUserName());
+ } else {
+ logger.warn("Intento de logout de '{}' con estado: {}", _loggedUser.getUserName(), res.getStatus());
+ }
+
+ JOptionPane.showMessageDialog(this,
+ res.getMessage().getMessage(),
+ res.getStatus().toString(),
+ res.getStatus() == SocketStatus.OK ?
+ JOptionPane.INFORMATION_MESSAGE : JOptionPane.ERROR_MESSAGE
+ );
+ }
+ } catch (IOException ex) {
+ logger.error("Error de conexión con el servidor al hacer logout de '{}': {}", _loggedUser.getUserName(), ex.getMessage());
+ JOptionPane.showMessageDialog(this,
+ "Error de conexión con el servidor",
+ "Error",
+ JOptionPane.ERROR_MESSAGE
+ );
+ }
+ }
+
+ clearMainInputs();
+ _loggedUser = null;
+ }
+
+ private void logoutBtn(ActionEvent e) {
+ logout();
+ setView("loginPanel");
+ }
+
+ private void thisWindowClosing(WindowEvent e) {
+ logout();
+ }
+
+ private void initComponents() {
+ // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents @formatter:off
+ // Generated using JFormDesigner Educational license - José Manuel Amador Gallardo (José Manuel Amador)
+ loginPanel = new JPanel();
+ logo = new JLabel();
+ welcomeLabel = new JLabel();
+ userPanel = new JPanel();
+ userLabel = new JLabel();
+ userTextField = new JTextField();
+ passwordPanel = new JPanel();
+ passwordLabel = new JLabel();
+ passwordTextField = new JPasswordField();
+ loginBtn = new JButton();
+ btnLoginPanel = new JPanel();
+ noAccountLabel = new JLabel();
+ registerBtn = new JButton();
+ versionLabel = new JLabel();
+ mainPanel = new JPanel();
+ destPanel = new JPanel();
+ destLabel = new JLabel();
+ destTextField = new JTextField();
+ amountPanel = new JPanel();
+ amountLabel = new JLabel();
+ amountTextField = new JTextField();
+ sendBtn = new JButton();
+ logoutBtn = new JButton();
+
+ //======== this ========
+ setResizable(false);
+ setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+ addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent e) {
+ thisWindowClosing(e);
+ }
+ });
+ var contentPane = getContentPane();
+ contentPane.setLayout(new CardLayout(12, 6));
+
+ //======== loginPanel ========
+ {
+ loginPanel.setLayout(new MigLayout(
+ "hidemode 3,gapy 12",
+ // columns
+ "[grow,fill]",
+ // rows
+ "[]0" +
+ "[]para" +
+ "[fill]" +
+ "[fill]para" +
+ "[]" +
+ "[grow,fill]"));
+
+ //---- logo ----
+ logo.setIcon(new ImageIcon(getClass().getResource("/images/banco.png")));
+ logo.setMaximumSize(new Dimension(256, 256));
+ logo.setMinimumSize(new Dimension(256, 256));
+ logo.setPreferredSize(new Dimension(256, 256));
+ logo.setHorizontalTextPosition(SwingConstants.CENTER);
+ loginPanel.add(logo, "cell 0 0,alignx center,growx 0");
+
+ //---- welcomeLabel ----
+ welcomeLabel.setText("Bienvenido a Banco Grupo 33. Inicie sesi\u00f3n.");
+ welcomeLabel.setFont(new Font("Adwaita Sans", Font.PLAIN, 16));
+ loginPanel.add(welcomeLabel, "cell 0 1,alignx center,growx 0");
+
+ //======== userPanel ========
+ {
+ userPanel.setLayout(new MigLayout(
+ "insets 0,hidemode 3",
+ // columns
+ "[fill]" +
+ "[grow,fill]",
+ // rows
+ "[grow,fill]"));
+
+ //---- userLabel ----
+ userLabel.setText("Usuario:");
+ userLabel.setFont(new Font("Adwaita Sans", Font.PLAIN, 18));
+ userLabel.setMaximumSize(new Dimension(130, 23));
+ userLabel.setMinimumSize(new Dimension(130, 23));
+ userLabel.setPreferredSize(new Dimension(130, 23));
+ userPanel.add(userLabel, "cell 0 0");
+
+ //---- userTextField ----
+ userTextField.setFont(new Font("Adwaita Sans", Font.PLAIN, 18));
+ userTextField.setCaretColor(new Color(0x076854));
+ userTextField.setSelectionColor(new Color(0x076854));
+ userPanel.add(userTextField, "cell 1 0");
+ }
+ loginPanel.add(userPanel, "cell 0 2");
+
+ //======== passwordPanel ========
+ {
+ passwordPanel.setLayout(new MigLayout(
+ "insets 0,hidemode 3",
+ // columns
+ "[fill]" +
+ "[grow,fill]",
+ // rows
+ "[grow,fill]"));
+
+ //---- passwordLabel ----
+ passwordLabel.setText("Contrase\u00f1a:");
+ passwordLabel.setFont(new Font("Adwaita Sans", Font.PLAIN, 18));
+ passwordLabel.setMaximumSize(new Dimension(130, 23));
+ passwordLabel.setMinimumSize(new Dimension(130, 23));
+ passwordLabel.setPreferredSize(new Dimension(130, 23));
+ passwordPanel.add(passwordLabel, "cell 0 0");
+
+ //---- passwordTextField ----
+ passwordTextField.setFont(new Font("Adwaita Sans", Font.PLAIN, 18));
+ passwordTextField.setSelectionColor(new Color(0x076854));
+ passwordTextField.setCaretColor(new Color(0x076854));
+ passwordPanel.add(passwordTextField, "cell 1 0");
+ }
+ loginPanel.add(passwordPanel, "cell 0 3");
+
+ //---- loginBtn ----
+ loginBtn.setText("Iniciar sesi\u00f3n");
+ loginBtn.setFont(new Font("Adwaita Sans", Font.PLAIN, 18));
+ loginBtn.addActionListener(e -> loginBtn(e));
+ loginPanel.add(loginBtn, "cell 0 4,alignx trailing,growx 0");
+
+ //======== btnLoginPanel ========
+ {
+ btnLoginPanel.setLayout(new MigLayout(
+ "hidemode 3,aligny bottom",
+ // columns
+ "[grow,fill]",
+ // rows
+ "[]" +
+ "[fill]para" +
+ "[]"));
+
+ //---- noAccountLabel ----
+ noAccountLabel.setText("\u00bfNo tiene cuenta?");
+ btnLoginPanel.add(noAccountLabel, "cell 0 0,alignx center,growx 0");
+
+ //---- registerBtn ----
+ registerBtn.setText("Registrarse");
+ registerBtn.setFont(new Font("Adwaita Sans", Font.PLAIN, 18));
+ registerBtn.addActionListener(e -> registerBtn(e));
+ btnLoginPanel.add(registerBtn, "cell 0 1,alignx center,growx 0");
+
+ //---- versionLabel ----
+ versionLabel.setForeground(new Color(0xa0a0a0));
+ btnLoginPanel.add(versionLabel, "cell 0 2,alignx center,growx 0");
+ }
+ loginPanel.add(btnLoginPanel, "cell 0 5");
+ }
+ contentPane.add(loginPanel, "loginPanel");
+
+ //======== mainPanel ========
+ {
+ mainPanel.setLayout(new MigLayout(
+ "hidemode 3",
+ // columns
+ "[grow,fill]",
+ // rows
+ "[grow,fill]" +
+ "[fill]" +
+ "[fill]" +
+ "[fill]" +
+ "[grow,fill]"));
+
+ //======== destPanel ========
+ {
+ destPanel.setLayout(new MigLayout(
+ "insets 0,hidemode 3",
+ // columns
+ "[fill]" +
+ "[grow,fill]",
+ // rows
+ "[]"));
+
+ //---- destLabel ----
+ destLabel.setText("Destinatario:");
+ destLabel.setFont(new Font("Adwaita Sans", Font.PLAIN, 18));
+ destLabel.setHorizontalAlignment(SwingConstants.LEFT);
+ destPanel.add(destLabel, "cell 0 0");
+ destPanel.add(destTextField, "cell 1 0");
+ }
+ mainPanel.add(destPanel, "cell 0 1");
+
+ //======== amountPanel ========
+ {
+ amountPanel.setLayout(new MigLayout(
+ "insets 0,hidemode 3",
+ // columns
+ "[fill]" +
+ "[grow,fill]",
+ // rows
+ "[grow,fill]"));
+
+ //---- amountLabel ----
+ amountLabel.setText("Cantidad:");
+ amountLabel.setFont(new Font("Adwaita Sans", Font.PLAIN, 18));
+ amountLabel.setMaximumSize(new Dimension(108, 23));
+ amountLabel.setMinimumSize(new Dimension(108, 23));
+ amountLabel.setPreferredSize(new Dimension(108, 23));
+ amountLabel.setHorizontalAlignment(SwingConstants.LEFT);
+ amountPanel.add(amountLabel, "cell 0 0");
+ amountPanel.add(amountTextField, "cell 1 0");
+ }
+ mainPanel.add(amountPanel, "cell 0 2");
+
+ //---- sendBtn ----
+ sendBtn.setText("Enviar");
+ sendBtn.setFont(new Font("Adwaita Sans", Font.PLAIN, 18));
+ sendBtn.addActionListener(e -> sendBtn(e));
+ mainPanel.add(sendBtn, "cell 0 3,alignx trailing,growx 0");
+
+ //---- logoutBtn ----
+ logoutBtn.setText("Cerrar sesi\u00f3n");
+ logoutBtn.setFont(new Font("Adwaita Sans", Font.PLAIN, 18));
+ logoutBtn.addActionListener(e -> logoutBtn(e));
+ mainPanel.add(logoutBtn, "cell 0 4,aligny bottom,growy 0");
+ }
+ contentPane.add(mainPanel, "mainPanel");
+ setSize(400, 600);
+ setLocationRelativeTo(getOwner());
+ // JFormDesigner - End of component initialization //GEN-END:initComponents @formatter:on
+ }
+
+ // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables @formatter:off
+ // Generated using JFormDesigner Educational license - José Manuel Amador Gallardo (José Manuel Amador)
+ private JPanel loginPanel;
+ private JLabel logo;
+ private JLabel welcomeLabel;
+ private JPanel userPanel;
+ private JLabel userLabel;
+ private JTextField userTextField;
+ private JPanel passwordPanel;
+ private JLabel passwordLabel;
+ private JPasswordField passwordTextField;
+ private JButton loginBtn;
+ private JPanel btnLoginPanel;
+ private JLabel noAccountLabel;
+ private JButton registerBtn;
+ private JLabel versionLabel;
+ private JPanel mainPanel;
+ private JPanel destPanel;
+ private JLabel destLabel;
+ private JTextField destTextField;
+ private JPanel amountPanel;
+ private JLabel amountLabel;
+ private JTextField amountTextField;
+ private JButton sendBtn;
+ private JButton logoutBtn;
+ // JFormDesigner - End of variables declaration //GEN-END:variables @formatter:on
+}
\ No newline at end of file
diff --git a/Integridos/src/main/java/net/miarma/integridos/client/ui/MainWindow.jfd b/Integridos/src/main/java/net/miarma/integridos/client/ui/MainWindow.jfd
new file mode 100644
index 0000000..a60b30c
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/client/ui/MainWindow.jfd
@@ -0,0 +1,209 @@
+JFDML JFormDesigner: "8.2.4.0.393" Java: "21.0.8" encoding: "UTF-8"
+
+new FormModel {
+ contentType: "form/swing"
+ root: new FormRoot {
+ add( new FormWindow( "javax.swing.JFrame", new FormLayoutManager( class java.awt.CardLayout ) {
+ "hgap": 12
+ "vgap": 6
+ } ) {
+ name: "this"
+ "$sizePolicy": 1
+ "resizable": false
+ "defaultCloseOperation": 3
+ addEvent( new FormEvent( "java.awt.event.WindowListener", "windowClosing", "thisWindowClosing", true ) )
+ add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
+ "$layoutConstraints": "hidemode 3,gapy 12"
+ "$columnConstraints": "[grow,fill]"
+ "$rowConstraints": "[]0[]para[fill][fill]para[][grow,fill]"
+ } ) {
+ name: "loginPanel"
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "logo"
+ "icon": new com.jformdesigner.model.SwingIcon( 0, "/images/banco.png" )
+ "maximumSize": new java.awt.Dimension( 256, 256 )
+ "minimumSize": new java.awt.Dimension( 256, 256 )
+ "preferredSize": new java.awt.Dimension( 256, 256 )
+ "horizontalTextPosition": 0
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 0,alignx center,growx 0"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "welcomeLabel"
+ "text": "Bienvenido a Banco Grupo 33. Inicie sesión."
+ "font": new java.awt.Font( "Adwaita Sans", 0, 16 )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 1,alignx center,growx 0"
+ } )
+ add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
+ "$layoutConstraints": "insets 0,hidemode 3"
+ "$columnConstraints": "[fill][grow,fill]"
+ "$rowConstraints": "[grow,fill]"
+ } ) {
+ name: "userPanel"
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "userLabel"
+ "text": "Usuario:"
+ "font": &Font0 new java.awt.Font( "Adwaita Sans", 0, 18 )
+ "maximumSize": new java.awt.Dimension( 130, 23 )
+ "minimumSize": new java.awt.Dimension( 130, 23 )
+ "preferredSize": new java.awt.Dimension( 130, 23 )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 0"
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "userTextField"
+ "font": &Font1 new java.awt.Font( "Adwaita Sans", 0, 18 )
+ "caretColor": &Color0 new java.awt.Color( 7, 104, 84, 255 )
+ "selectionColor": new java.awt.Color( 7, 104, 84, 255 )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 0"
+ } )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 2"
+ } )
+ add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
+ "$layoutConstraints": "insets 0,hidemode 3"
+ "$columnConstraints": "[fill][grow,fill]"
+ "$rowConstraints": "[grow,fill]"
+ } ) {
+ name: "passwordPanel"
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "passwordLabel"
+ "text": "Contraseña:"
+ "font": #Font0
+ "maximumSize": new java.awt.Dimension( 130, 23 )
+ "minimumSize": new java.awt.Dimension( 130, 23 )
+ "preferredSize": new java.awt.Dimension( 130, 23 )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 0"
+ } )
+ add( new FormComponent( "javax.swing.JPasswordField" ) {
+ name: "passwordTextField"
+ "font": #Font1
+ "selectionColor": new java.awt.Color( 7, 104, 84, 255 )
+ "caretColor": #Color0
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 0"
+ } )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 3"
+ } )
+ add( new FormComponent( "javax.swing.JButton" ) {
+ name: "loginBtn"
+ "text": "Iniciar sesión"
+ "font": &Font2 new java.awt.Font( "Adwaita Sans", 0, 18 )
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "loginBtn", true ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 4,alignx trailing,growx 0"
+ } )
+ add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
+ "$layoutConstraints": "hidemode 3,aligny bottom"
+ "$columnConstraints": "[grow,fill]"
+ "$rowConstraints": "[][fill]para[]"
+ } ) {
+ name: "btnLoginPanel"
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "noAccountLabel"
+ "text": "¿No tiene cuenta?"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 0,alignx center,growx 0"
+ } )
+ add( new FormComponent( "javax.swing.JButton" ) {
+ name: "registerBtn"
+ "text": "Registrarse"
+ "font": #Font2
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "registerBtn", true ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 1,alignx center,growx 0"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "versionLabel"
+ "foreground": new java.awt.Color( 160, 160, 160, 255 )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 2,alignx center,growx 0"
+ } )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 5"
+ } )
+ }, new FormLayoutConstraints( class java.lang.String ) {
+ "value": "loginPanel"
+ } )
+ add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
+ "$layoutConstraints": "hidemode 3"
+ "$columnConstraints": "[grow,fill]"
+ "$rowConstraints": "[grow,fill][fill][fill][fill][grow,fill]"
+ } ) {
+ name: "mainPanel"
+ add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
+ "$layoutConstraints": "insets 0,hidemode 3"
+ "$columnConstraints": "[fill][grow,fill]"
+ "$rowConstraints": "[]"
+ } ) {
+ name: "destPanel"
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "destLabel"
+ "text": "Destinatario:"
+ "font": &Font3 new java.awt.Font( "Adwaita Sans", 0, 18 )
+ "horizontalAlignment": 2
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 0"
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "destTextField"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 0"
+ } )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 1"
+ } )
+ add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
+ "$layoutConstraints": "insets 0,hidemode 3"
+ "$columnConstraints": "[fill][grow,fill]"
+ "$rowConstraints": "[grow,fill]"
+ } ) {
+ name: "amountPanel"
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "amountLabel"
+ "text": "Cantidad:"
+ "font": #Font3
+ "maximumSize": new java.awt.Dimension( 108, 23 )
+ "minimumSize": new java.awt.Dimension( 108, 23 )
+ "preferredSize": new java.awt.Dimension( 108, 23 )
+ "horizontalAlignment": 2
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 0"
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "amountTextField"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 0"
+ } )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 2"
+ } )
+ add( new FormComponent( "javax.swing.JButton" ) {
+ name: "sendBtn"
+ "text": "Enviar"
+ "font": &Font4 new java.awt.Font( "Adwaita Sans", 0, 18 )
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "sendBtn", true ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 3,alignx trailing,growx 0"
+ } )
+ add( new FormComponent( "javax.swing.JButton" ) {
+ name: "logoutBtn"
+ "text": "Cerrar sesión"
+ "font": #Font4
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "logoutBtn", true ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 4,aligny bottom,growy 0"
+ } )
+ }, new FormLayoutConstraints( class java.lang.String ) {
+ "value": "mainPanel"
+ } )
+ }, new FormLayoutConstraints( null ) {
+ "location": new java.awt.Point( 0, 0 )
+ "size": new java.awt.Dimension( 400, 600 )
+ } )
+ }
+}
diff --git a/Integridos/src/main/java/net/miarma/integridos/common/ConfigManager.java b/Integridos/src/main/java/net/miarma/integridos/common/ConfigManager.java
new file mode 100644
index 0000000..67bf78d
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/common/ConfigManager.java
@@ -0,0 +1,105 @@
+package net.miarma.integridos.common;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Properties;
+
+/**
+ * Gestión de toda la configuración de la aplicación.
+ * Se encarga de cargar, guardar y proporcionar acceso a las propiedades de configuración.
+ * Proporciona métodos para obtener la URL de la base de datos, directorios de archivos,
+ * y propiedades específicas como host, puerto, etc.
+ *
+ * Esta clase sigue el patron Singleton para asegurar una sola instancia.
+ * @author José Manuel Amador Gallardo
+ */
+public class ConfigManager {
+ private static ConfigManager INSTANCE;
+ private final File configFile;
+ private final Properties config;
+ private static final String CONFIG_FILE_NAME = "config.properties";
+ private final Logger logger = LoggerFactory.getLogger(ConfigManager.class);
+
+ private ConfigManager() {
+ String path = getBaseDir() + CONFIG_FILE_NAME;
+ this.configFile = new File(path);
+ this.config = new Properties();
+ }
+
+ public static ConfigManager getInstance() {
+ if (INSTANCE == null) {
+ INSTANCE = new ConfigManager();
+ }
+ return INSTANCE;
+ }
+
+ public void loadConfig() {
+ try (FileInputStream fis = new FileInputStream(configFile);
+ InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
+ config.load(isr);
+ } catch (IOException e) {
+ logger.error("Error loading configuration file: ", e);
+ }
+ }
+ public File getConfigFile() {
+ return configFile;
+ }
+
+ public String getHomeDir() {
+ return getOS() == OSType.WINDOWS ?
+ "C:/Users/" + System.getProperty("user.name") + "/" :
+ System.getProperty("user.home").contains("root") ? "/root/" :
+ "/home/" + System.getProperty("user.name") + "/";
+ }
+
+ public String getBaseDir() {
+ return getHomeDir() +
+ (getOS() == OSType.WINDOWS ? ".integridos/" :
+ getOS() == OSType.LINUX ? ".config/integridos/" :
+ ".integridos/");
+ }
+
+ public static OSType getOS() {
+ String os = System.getProperty("os.name").toLowerCase();
+ if (os.contains("win")) {
+ return OSType.WINDOWS;
+ } else if (os.contains("nix") || os.contains("nux")) {
+ return OSType.LINUX;
+ } else {
+ return OSType.INVALID_OS;
+ }
+ }
+
+ public String getStringProperty(String key) {
+ return config.getProperty(key);
+ }
+
+ public int getIntProperty(String key) {
+ String value = config.getProperty(key);
+ return value != null ? Integer.parseInt(value) : 10;
+ }
+
+ public boolean getBooleanProperty(String key) {
+ return Boolean.parseBoolean(config.getProperty(key));
+ }
+
+ public void setProperty(String key, String value) {
+ config.setProperty(key, value);
+ saveConfig();
+ }
+
+ private void saveConfig() {
+ try (FileOutputStream fos = new FileOutputStream(configFile)) {
+ config.store(fos, "Configuration for: " + Constants.APP_NAME);
+ } catch (IOException e) {
+ logger.error("Error saving configuration file: ", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Integridos/src/main/java/net/miarma/integridos/common/Constants.java b/Integridos/src/main/java/net/miarma/integridos/common/Constants.java
new file mode 100644
index 0000000..6997f6d
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/common/Constants.java
@@ -0,0 +1,10 @@
+package net.miarma.integridos.common;
+
+public class Constants {
+ public static final String APP_NAME = "Integridos";
+ public static final String APP_VERSION = "1.0.0";
+
+ private Constants() {
+ throw new AssertionError("Utility class cannot be instantiated.");
+ }
+}
\ No newline at end of file
diff --git a/Integridos/src/main/java/net/miarma/integridos/common/OSType.java b/Integridos/src/main/java/net/miarma/integridos/common/OSType.java
new file mode 100644
index 0000000..4e6b65f
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/common/OSType.java
@@ -0,0 +1,9 @@
+package net.miarma.integridos.common;
+
+/**
+ * Enum que representa los diferentes tipos de sistemas operativos soportados
+ * @author José Manuel Amador Gallardo
+ */
+public enum OSType {
+ LINUX, WINDOWS, INVALID_OS
+}
\ No newline at end of file
diff --git a/Integridos/src/main/java/net/miarma/integridos/common/db/DBConnector.java b/Integridos/src/main/java/net/miarma/integridos/common/db/DBConnector.java
new file mode 100644
index 0000000..bce1ff6
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/common/db/DBConnector.java
@@ -0,0 +1,31 @@
+package net.miarma.integridos.common.db;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.EntityManagerFactory;
+import jakarta.persistence.Persistence;
+
+public class DBConnector {
+ private static DBConnector instance;
+ private EntityManagerFactory emf;
+
+ private DBConnector() {
+ this.emf = Persistence.createEntityManagerFactory("ssii-pai1");
+ }
+
+ public static DBConnector getInstance() {
+ if (instance == null) {
+ instance = new DBConnector();
+ }
+ return instance;
+ }
+
+ public EntityManager createEntityManager() {
+ return emf.createEntityManager();
+ }
+
+ public void close() {
+ if (emf != null && emf.isOpen()) {
+ emf.close();
+ }
+ }
+}
diff --git a/Integridos/src/main/java/net/miarma/integridos/common/db/DBPopulator.java b/Integridos/src/main/java/net/miarma/integridos/common/db/DBPopulator.java
new file mode 100644
index 0000000..271aeb3
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/common/db/DBPopulator.java
@@ -0,0 +1,51 @@
+package net.miarma.integridos.common.db;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.TypedQuery;
+import net.miarma.integridos.common.security.PasswordHasher;
+import net.miarma.integridos.common.db.entities.UserEntity;
+
+import java.util.UUID;
+
+public class DBPopulator {
+ private final DBConnector conn = DBConnector.getInstance();
+ private EntityManager em;
+ private static DBPopulator instance;
+
+ private DBPopulator() {
+ em = conn.createEntityManager();
+ }
+
+ public static DBPopulator getInstance() {
+ if (instance == null) {
+ instance = new DBPopulator();
+ }
+ return instance;
+ }
+
+ public boolean isFirstRun() {
+ TypedQuery q = em.createQuery("SELECT COUNT(u) FROM UserEntity u", Long.class);
+ Long count = q.getSingleResult();
+ return count == 0;
+ }
+
+ public void populate() {
+ UserEntity u1 = new UserEntity(UUID.randomUUID().toString(),
+ "ricolinad", PasswordHasher.hash("ricardo2025"));
+ UserEntity u2 = new UserEntity(UUID.randomUUID().toString(),
+ "alvgulveg", PasswordHasher.hash("alcalde_dimision"));
+ UserEntity u3 = new UserEntity(UUID.randomUUID().toString(),
+ "smt4497", PasswordHasher.hash("quemenlaus"));
+ try {
+ em.getTransaction().begin();
+ em.persist(u1);
+ em.persist(u2);
+ em.persist(u3);
+ em.getTransaction().commit();
+ } catch (Exception e) {
+ em.getTransaction().rollback();
+ } finally {
+ em.close();
+ }
+ }
+}
diff --git a/Integridos/src/main/java/net/miarma/integridos/common/db/dao/SessionDAO.java b/Integridos/src/main/java/net/miarma/integridos/common/db/dao/SessionDAO.java
new file mode 100644
index 0000000..0781d82
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/common/db/dao/SessionDAO.java
@@ -0,0 +1,20 @@
+package net.miarma.integridos.common.db.dao;
+
+import jakarta.persistence.EntityManager;
+import net.miarma.integridos.common.db.entities.SessionEntity;
+
+public class SessionDAO {
+ public SessionEntity getByUserId(EntityManager em, String userId) {
+ return em.createQuery("SELECT s FROM SessionEntity s WHERE s.userId = :userId", SessionEntity.class)
+ .setParameter("userId", userId)
+ .getSingleResult();
+ }
+
+ public boolean isLoggedIn(EntityManager em, String userId) {
+ Long count = em.createQuery(
+ "SELECT COUNT(s) FROM SessionEntity s WHERE s.userId = :userId", Long.class)
+ .setParameter("userId", userId)
+ .getSingleResult();
+ return count > 0;
+ }
+}
diff --git a/Integridos/src/main/java/net/miarma/integridos/common/db/dao/TransactionDAO.java b/Integridos/src/main/java/net/miarma/integridos/common/db/dao/TransactionDAO.java
new file mode 100644
index 0000000..27d1761
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/common/db/dao/TransactionDAO.java
@@ -0,0 +1,13 @@
+package net.miarma.integridos.common.db.dao;
+
+import jakarta.persistence.EntityManager;
+
+public class TransactionDAO {
+ public boolean isNonceUsed(EntityManager em, String nonce) {
+ Long count = em.createQuery(
+ "SELECT COUNT(t) FROM TransactionEntity t WHERE t.nonce = :nonce", Long.class)
+ .setParameter("nonce", nonce)
+ .getSingleResult();
+ return count > 0;
+ }
+}
diff --git a/Integridos/src/main/java/net/miarma/integridos/common/db/dao/UserDAO.java b/Integridos/src/main/java/net/miarma/integridos/common/db/dao/UserDAO.java
new file mode 100644
index 0000000..285c4ac
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/common/db/dao/UserDAO.java
@@ -0,0 +1,16 @@
+package net.miarma.integridos.common.db.dao;
+
+import jakarta.persistence.EntityManager;
+import net.miarma.integridos.common.db.entities.UserEntity;
+
+public class UserDAO {
+ public UserEntity getByUserName(EntityManager em, String userName) {
+ try {
+ return em.createQuery("SELECT u FROM UserEntity u WHERE u.userName = :userName", UserEntity.class)
+ .setParameter("userName", userName)
+ .getSingleResult();
+ } catch (jakarta.persistence.NoResultException e) {
+ return null; // No se encontró, devolvemos null
+ }
+ }
+}
diff --git a/Integridos/src/main/java/net/miarma/integridos/common/db/dto/LoginDTO.java b/Integridos/src/main/java/net/miarma/integridos/common/db/dto/LoginDTO.java
new file mode 100644
index 0000000..0f88316
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/common/db/dto/LoginDTO.java
@@ -0,0 +1,30 @@
+package net.miarma.integridos.common.db.dto;
+
+public class LoginDTO {
+ private String username;
+ private String password;
+
+ public LoginDTO() {
+ }
+
+ public LoginDTO(String username, String password) {
+ this.username = username;
+ this.password = password;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+}
diff --git a/Integridos/src/main/java/net/miarma/integridos/common/db/dto/TransactionDTO.java b/Integridos/src/main/java/net/miarma/integridos/common/db/dto/TransactionDTO.java
new file mode 100644
index 0000000..8b50f4c
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/common/db/dto/TransactionDTO.java
@@ -0,0 +1,58 @@
+package net.miarma.integridos.common.db.dto;
+
+public class TransactionDTO {
+ private String fromUserName;
+ private String toUserName;
+ private double amount;
+ private String hmac;
+ private String nonce;
+
+ public TransactionDTO() {}
+
+ public TransactionDTO(String fromUserName, String toUserName, double amount, String nonce) {
+ this.fromUserName = fromUserName;
+ this.toUserName = toUserName;
+ this.amount = amount;
+ this.nonce = nonce;
+ }
+
+ public String getFromUserName() {
+ return fromUserName;
+ }
+
+ public void setFromUserName(String fromUserName) {
+ this.fromUserName = fromUserName;
+ }
+
+ public String getToUserName() {
+ return toUserName;
+ }
+
+ public void setToUserName(String toUserName) {
+ this.toUserName = toUserName;
+ }
+
+ public double getAmount() {
+ return amount;
+ }
+
+ public void setAmount(double amount) {
+ this.amount = amount;
+ }
+
+ public String getHmac() {
+ return hmac;
+ }
+
+ public void setHmac(String hmac) {
+ this.hmac = hmac;
+ }
+
+ public String getNonce() {
+ return nonce;
+ }
+
+ public void setNonce(String nonce) {
+ this.nonce = nonce;
+ }
+}
\ No newline at end of file
diff --git a/Integridos/src/main/java/net/miarma/integridos/common/db/entities/SessionEntity.java b/Integridos/src/main/java/net/miarma/integridos/common/db/entities/SessionEntity.java
new file mode 100644
index 0000000..c63e414
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/common/db/entities/SessionEntity.java
@@ -0,0 +1,66 @@
+package net.miarma.integridos.common.db.entities;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "sessions")
+public class SessionEntity {
+ @Id
+ @Column(name = "sessionId", nullable = false, updatable = false)
+ private String sessionId;
+
+ @Column(name = "userId", nullable = false)
+ private String userId;
+
+ @Column(name = "createdAt", nullable = false, updatable = false, insertable = false)
+ private LocalDateTime createdAt;
+
+ @Column(name = "expiresAt", nullable = false, updatable = false, insertable = false)
+ private LocalDateTime expiresAt;
+
+ public SessionEntity() {}
+
+ public SessionEntity(String sessionId, String userId, LocalDateTime createdAt, LocalDateTime expiresAt) {
+ this.sessionId = sessionId;
+ this.userId = userId;
+ this.createdAt = createdAt;
+ this.expiresAt = expiresAt;
+ }
+
+ public String getSessionId() {
+ return sessionId;
+ }
+
+ public void setSessionId(String sessionId) {
+ this.sessionId = sessionId;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public LocalDateTime getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(LocalDateTime createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public LocalDateTime getExpiresAt() {
+ return expiresAt;
+ }
+
+ public void setExpiresAt(LocalDateTime expiresAt) {
+ this.expiresAt = expiresAt;
+ }
+}
\ No newline at end of file
diff --git a/Integridos/src/main/java/net/miarma/integridos/common/db/entities/TransactionEntity.java b/Integridos/src/main/java/net/miarma/integridos/common/db/entities/TransactionEntity.java
new file mode 100644
index 0000000..e815993
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/common/db/entities/TransactionEntity.java
@@ -0,0 +1,85 @@
+package net.miarma.integridos.common.db.entities;
+
+import jakarta.persistence.*;
+
+@Entity
+@Table(name = "transactions")
+public class TransactionEntity {
+ @Id
+ @Column(name = "transactionId", nullable = false, updatable = false, length = 36)
+ private String transactionId;
+
+ @ManyToOne
+ @JoinColumn(name = "fromUser", referencedColumnName = "userId", nullable = false, updatable = false)
+ private UserEntity fromUser;
+
+ @ManyToOne
+ @JoinColumn(name = "toUser", referencedColumnName = "userId", nullable = false, updatable = false)
+ private UserEntity toUser;
+
+ @Column(name = "transactionAmount", nullable = false, updatable = false)
+ private Double transactionAmount;
+
+ @Column(name = "nonce", nullable = false, updatable = false)
+ private String nonce;
+
+ public TransactionEntity() {}
+
+ public TransactionEntity(String transactionId, UserEntity fromUser, UserEntity toUser, Double transactionAmount) {
+ this.transactionId = transactionId;
+ this.fromUser = fromUser;
+ this.toUser = toUser;
+ this.transactionAmount = transactionAmount;
+ }
+
+ public String getTransactionId() {
+ return transactionId;
+ }
+
+ public void setTransactionId(String transactionId) {
+ this.transactionId = transactionId;
+ }
+
+ public UserEntity getFromUser() {
+ return fromUser;
+ }
+
+ public void setFromUser(UserEntity fromUser) {
+ this.fromUser = fromUser;
+ }
+
+ public UserEntity getToUser() {
+ return toUser;
+ }
+
+ public void setToUser(UserEntity toUser) {
+ this.toUser = toUser;
+ }
+
+ public Double getTransactionAmount() {
+ return transactionAmount;
+ }
+
+ public void setTransactionAmount(Double transactionAmount) {
+ this.transactionAmount = transactionAmount;
+ }
+
+ public String getNonce() {
+ return nonce;
+ }
+
+ public void setNonce(String nonce) {
+ this.nonce = nonce;
+ }
+
+ @Override
+ public String toString() {
+ return "TransactionEntity{" +
+ "transactionId='" + transactionId + '\'' +
+ ", fromUser=" + fromUser +
+ ", toUser=" + toUser +
+ ", transactionAmount=" + transactionAmount +
+ ", nonce=" + nonce +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/Integridos/src/main/java/net/miarma/integridos/common/db/entities/UserEntity.java b/Integridos/src/main/java/net/miarma/integridos/common/db/entities/UserEntity.java
new file mode 100644
index 0000000..b0cfdbd
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/common/db/entities/UserEntity.java
@@ -0,0 +1,63 @@
+package net.miarma.integridos.common.db.entities;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+@Entity
+@Table(name = "users")
+public class UserEntity {
+ @Id
+ @Column(name = "userId", nullable = false, updatable = false, length = 36)
+ private String userId;
+
+ @Column(name = "userName", unique = true, nullable = false, length = 64)
+ private String userName;
+
+ @Column(name = "password", nullable = false, length = 256)
+ private String password;
+
+ @Column(name = "balance", nullable = false)
+ private Double balance = 0.00;
+
+ public UserEntity() {}
+
+ public UserEntity(String userId, String userName, String password) {
+ this.userId = userId;
+ this.userName = userName;
+ this.password = password;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public void setUserName(String userName) {
+ this.userName = userName;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public Double getBalance() {
+ return balance;
+ }
+
+ public void setBalance(Double balance) {
+ this.balance = balance;
+ }
+}
\ No newline at end of file
diff --git a/Integridos/src/main/java/net/miarma/integridos/common/security/IntegrityProvider.java b/Integridos/src/main/java/net/miarma/integridos/common/security/IntegrityProvider.java
new file mode 100644
index 0000000..7dbe943
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/common/security/IntegrityProvider.java
@@ -0,0 +1,30 @@
+package net.miarma.integridos.common.security;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.Base64;
+
+public class IntegrityProvider {
+ public static String generateHMAC(String key, String data) throws Exception {
+ Mac sha256HMAC = Mac.getInstance("HmacSHA256");
+ SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
+ sha256HMAC.init(secretKey);
+ return byteArrayToHex(sha256HMAC.doFinal(data.getBytes(StandardCharsets.UTF_8)));
+ }
+
+ public static String generateNonce() {
+ byte[] nonce = new byte[16];
+ new SecureRandom().nextBytes(nonce);
+ return Base64.getEncoder().encodeToString(nonce);
+ }
+
+ public static String byteArrayToHex(byte[] arr) {
+ StringBuilder sb = new StringBuilder(arr.length * 2);
+ for(byte b : arr) {
+ sb.append(String.format("%02x", b));
+ }
+ return sb.toString();
+ }
+}
diff --git a/Integridos/src/main/java/net/miarma/integridos/common/security/PasswordHasher.java b/Integridos/src/main/java/net/miarma/integridos/common/security/PasswordHasher.java
new file mode 100644
index 0000000..4b0d4e3
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/common/security/PasswordHasher.java
@@ -0,0 +1,22 @@
+package net.miarma.integridos.common.security;
+
+import org.mindrot.jbcrypt.BCrypt;
+
+/**
+ * Clase de utilidad para el hash de contraseñas.
+ * Utiliza BCrypt para generar y verificar hashes de contraseñas.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class PasswordHasher {
+
+ private static final int SALT_ROUNDS = 12;
+
+ public static String hash(String plainPassword) {
+ return BCrypt.hashpw(plainPassword, BCrypt.gensalt(SALT_ROUNDS));
+ }
+
+ public static boolean verify(String plainPassword, String hashedPassword) {
+ return BCrypt.checkpw(plainPassword, hashedPassword);
+ }
+}
\ No newline at end of file
diff --git a/Integridos/src/main/java/net/miarma/integridos/common/socket/SocketRequest.java b/Integridos/src/main/java/net/miarma/integridos/common/socket/SocketRequest.java
new file mode 100644
index 0000000..8656ac2
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/common/socket/SocketRequest.java
@@ -0,0 +1,41 @@
+package net.miarma.integridos.common.socket;
+
+public class SocketRequest {
+ private SocketRequest.Action action;
+ private Object payload;
+
+ public SocketRequest() {}
+
+ public SocketRequest(SocketRequest.Action action, Object payload) {
+ this.action = action;
+ this.payload = payload;
+ }
+
+ public SocketRequest.Action getAction() {
+ return action;
+ }
+
+ public void setAction(SocketRequest.Action action) {
+ this.action = action;
+ }
+
+ public Object getPayload() {
+ return payload;
+ }
+
+ public void setPayload(Object payload) {
+ this.payload = payload;
+ }
+
+ @Override
+ public String toString() {
+ return "SocketRequest{" +
+ "action='" + action.name() + '\'' +
+ ", payload=" + (payload != null ? payload.toString() : "null") +
+ '}';
+ }
+
+ public enum Action {
+ LOGIN, REGISTER, SEND_TRANSACTION, LOGOUT
+ }
+}
diff --git a/Integridos/src/main/java/net/miarma/integridos/common/socket/SocketResponse.java b/Integridos/src/main/java/net/miarma/integridos/common/socket/SocketResponse.java
new file mode 100644
index 0000000..f34da53
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/common/socket/SocketResponse.java
@@ -0,0 +1,80 @@
+package net.miarma.integridos.common.socket;
+
+public class SocketResponse {
+
+ private SocketStatus status;
+ private Message message;
+ private Object data;
+
+ public SocketResponse() {}
+
+ public SocketResponse(SocketStatus status) {
+ this(status, (Message) null, null);
+ }
+
+ public SocketResponse(SocketStatus status, Object data) {
+ this(status, null, data);
+ }
+
+ public SocketResponse(SocketStatus status, Message message) {
+ this(status, message, message != null ? message.getMessage() : null);
+ }
+
+ public SocketResponse(SocketStatus status, Exception e) {
+ this(status, Message.UNKNOWN_ERROR, e.getMessage());
+ }
+
+ public SocketResponse(SocketStatus status, Message message, Object data) {
+ this.status = status;
+ this.message = message;
+ this.data = data;
+ }
+
+ public SocketStatus getStatus() {
+ return status;
+ }
+
+ public Message getMessage() {
+ return message;
+ }
+
+ public Object getData() {
+ return data;
+ }
+
+ @Override
+ public String toString() {
+ return "SocketResponse{" +
+ "status=" + status +
+ ", message=" + (message != null ? message.name() : "null") +
+ ", data=" + (data != null ? data.toString() : "null") +
+ '}';
+ }
+
+ public enum Message {
+ LOGGED_OUT("Ha cerrado sesión correctamente"),
+ LOGGED_IN("Ha iniciado sesión correctamente"),
+ TRANSACTION_SENT("Su transacción se ha realizado"),
+ USER_NOT_FOUND("El usuario no existe."),
+ WRONG_PASSWORD("Contraseña incorrecta."),
+ USER_ALREADY_EXISTS("El usuario ya está registrado."),
+ CONNECTION_ERROR("Error de conexión con el servidor."),
+ UNKNOWN_ERROR("Ha ocurrido un error inesperado."),
+ SESSION_EXPIRED("Tu sesión ha expirado."),
+ INTEGRITY_FAIL("Fallo de integridad en el mensaje."),
+ INVALID_AMOUNT("Cantidad no válida."),
+ REPLAY_ATTACK("Intento de repetición detectado."),
+ RECIPIENT_NOT_FOUND("El destinatario no existe"),
+ REGISTERED("Se ha registrado correctamente");
+
+ private final String message;
+
+ Message(String userMessage) {
+ this.message = userMessage;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+ }
+}
diff --git a/Integridos/src/main/java/net/miarma/integridos/common/socket/SocketStatus.java b/Integridos/src/main/java/net/miarma/integridos/common/socket/SocketStatus.java
new file mode 100644
index 0000000..62d2765
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/common/socket/SocketStatus.java
@@ -0,0 +1,14 @@
+package net.miarma.integridos.common.socket;
+
+public enum SocketStatus {
+ OK,
+ ERROR,
+ SESSION_EXPIRED,
+ INTEGRITY_FAIL,
+ UNAUTHORIZED,
+ INVALID_REQUEST;
+
+ public static boolean isOk(String status) {
+ return OK.name().equalsIgnoreCase(status);
+ }
+}
diff --git a/Integridos/src/main/java/net/miarma/integridos/server/RemoteSocket.java b/Integridos/src/main/java/net/miarma/integridos/server/RemoteSocket.java
new file mode 100644
index 0000000..5cd61fa
--- /dev/null
+++ b/Integridos/src/main/java/net/miarma/integridos/server/RemoteSocket.java
@@ -0,0 +1,207 @@
+package net.miarma.integridos.server;
+
+import com.google.gson.Gson;
+import jakarta.persistence.EntityManager;
+import net.miarma.integridos.common.*;
+import net.miarma.integridos.common.db.entities.TransactionEntity;
+import net.miarma.integridos.common.security.IntegrityProvider;
+import net.miarma.integridos.common.security.PasswordHasher;
+import net.miarma.integridos.common.socket.SocketRequest;
+import net.miarma.integridos.common.socket.SocketResponse;
+import net.miarma.integridos.common.socket.SocketStatus;
+import net.miarma.integridos.common.db.DBConnector;
+import net.miarma.integridos.common.db.dao.SessionDAO;
+import net.miarma.integridos.common.db.dao.TransactionDAO;
+import net.miarma.integridos.common.db.dao.UserDAO;
+import net.miarma.integridos.common.db.dto.LoginDTO;
+import net.miarma.integridos.common.db.dto.TransactionDTO;
+import net.miarma.integridos.common.db.entities.SessionEntity;
+import net.miarma.integridos.common.db.entities.UserEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.UUID;
+
+public class RemoteSocket {
+ static void main(String[] args) throws IOException {
+ ServerSocket serverSocket = new ServerSocket(6969);
+ DBConnector conn = DBConnector.getInstance();
+ UserDAO userDAO = new UserDAO();
+ SessionDAO sessionDAO = new SessionDAO();
+ TransactionDAO transactionDAO = new TransactionDAO();
+ Gson gson = new Gson();
+ final Logger logger = LoggerFactory.getLogger(RemoteSocket.class);
+ ConfigManager.getInstance().loadConfig();
+
+ while (true) {
+ Socket socket = serverSocket.accept();
+ logger.info("Nueva conexión desde {}:{}", socket.getInetAddress(), socket.getPort());
+ BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+ PrintWriter output = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true);
+
+ try {
+ SocketRequest msg = gson.fromJson(input.readLine(), SocketRequest.class);
+ SocketResponse res;
+
+ switch (msg.getAction()) {
+ case REGISTER -> {
+ LoginDTO dto = gson.fromJson(gson.toJson(msg.getPayload()), LoginDTO.class);
+ logger.info("Procesando REGISTER para {}", dto.getUsername());
+ EntityManager em = conn.createEntityManager();
+ try {
+ if (userDAO.getByUserName(em, dto.getUsername()) != null) {
+ res = new SocketResponse(SocketStatus.INVALID_REQUEST, SocketResponse.Message.USER_ALREADY_EXISTS);
+ } else {
+ em.getTransaction().begin();
+ UserEntity u = new UserEntity(UUID.randomUUID().toString(),
+ dto.getUsername(), PasswordHasher.hash(dto.getPassword()));
+ em.persist(u);
+ em.getTransaction().commit();
+ res = new SocketResponse(SocketStatus.OK, SocketResponse.Message.REGISTERED, u);
+ }
+ } catch (Exception e) {
+ if (em.getTransaction().isActive()) em.getTransaction().rollback();
+ logger.error(e.getMessage(), e);
+ res = new SocketResponse(SocketStatus.ERROR, SocketResponse.Message.UNKNOWN_ERROR, e.getMessage());
+ } finally {
+ em.close();
+ }
+ }
+
+ case LOGIN -> {
+ LoginDTO dto = gson.fromJson(gson.toJson(msg.getPayload()), LoginDTO.class);
+ logger.info("Procesando LOGIN para {}", dto.getUsername());
+ EntityManager em = conn.createEntityManager();
+ try {
+ UserEntity user = userDAO.getByUserName(em, dto.getUsername());
+ if (user == null) {
+ res = new SocketResponse(SocketStatus.INVALID_REQUEST, SocketResponse.Message.USER_NOT_FOUND);
+ } else if (PasswordHasher.verify(dto.getPassword(), user.getPassword())) {
+ res = new SocketResponse(SocketStatus.OK, SocketResponse.Message.LOGGED_IN, user);
+
+ SessionEntity session = new SessionEntity();
+ session.setSessionId(UUID.randomUUID().toString());
+ session.setUserId(user.getUserId());
+
+ em.getTransaction().begin();
+ em.persist(session);
+ em.getTransaction().commit();
+ } else {
+ res = new SocketResponse(SocketStatus.INVALID_REQUEST, SocketResponse.Message.WRONG_PASSWORD);
+ }
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ res = new SocketResponse(SocketStatus.ERROR, SocketResponse.Message.UNKNOWN_ERROR, e.getMessage());
+ } finally {
+ em.close();
+ }
+ }
+
+ case LOGOUT -> {
+ String userId = gson.fromJson(gson.toJson(msg.getPayload()), String.class);
+ logger.info("Procesando LOGOUT para {}", userId);
+ EntityManager em = conn.createEntityManager();
+ try {
+ SessionEntity session = sessionDAO.getByUserId(em, userId);
+ if (session != null) {
+ em.getTransaction().begin();
+ em.remove(session);
+ em.getTransaction().commit();
+ res = new SocketResponse(SocketStatus.OK, SocketResponse.Message.LOGGED_OUT);
+ } else {
+ res = new SocketResponse(SocketStatus.SESSION_EXPIRED, SocketResponse.Message.SESSION_EXPIRED);
+ }
+ } finally {
+ em.close();
+ }
+ }
+
+ case SEND_TRANSACTION -> {
+ TransactionDTO dto = gson.fromJson(gson.toJson(msg.getPayload()), TransactionDTO.class);
+
+ logger.info("Procesando SEND_TRANSACTION de {} a {} | {}", dto.getFromUserName(), dto.getToUserName(), dto.getAmount());
+
+ String receivedHmac = dto.getHmac();
+
+ logger.debug("HMAC recibida: {}", receivedHmac);
+
+ dto.setHmac(null);
+ msg.setPayload(dto);
+ String jsonMsg = gson.toJson(msg);
+
+ logger.debug("Mensaje JSON para calcular HMAC: {}", jsonMsg);
+
+ String generatedHmac = IntegrityProvider.generateHMAC(
+ ConfigManager.getInstance().getStringProperty("secret"), jsonMsg
+ );
+
+ logger.debug("HMAC generada: {}", generatedHmac);
+
+ if (!generatedHmac.equals(receivedHmac)) {
+ res = new SocketResponse(SocketStatus.INTEGRITY_FAIL, SocketResponse.Message.INTEGRITY_FAIL);
+ break;
+ }
+
+ logger.debug("Las HMAC coinciden");
+
+ EntityManager em = conn.createEntityManager();
+ try {
+ if (transactionDAO.isNonceUsed(em, dto.getNonce())) {
+ res = new SocketResponse(SocketStatus.INTEGRITY_FAIL, SocketResponse.Message.REPLAY_ATTACK);
+ break;
+ }
+
+ em.getTransaction().begin();
+ UserEntity fromUser = userDAO.getByUserName(em, dto.getFromUserName());
+ UserEntity toUser = userDAO.getByUserName(em, dto.getToUserName());
+
+ if (fromUser == null || !sessionDAO.isLoggedIn(em, fromUser.getUserId())) {
+ res = new SocketResponse(SocketStatus.SESSION_EXPIRED, SocketResponse.Message.SESSION_EXPIRED);
+ } else if (toUser == null) {
+ res = new SocketResponse(SocketStatus.INVALID_REQUEST, SocketResponse.Message.RECIPIENT_NOT_FOUND);
+ } else if (dto.getAmount() <= 0 || dto.getAmount() > fromUser.getBalance()) {
+ res = new SocketResponse(SocketStatus.INVALID_REQUEST, SocketResponse.Message.INVALID_AMOUNT);
+ } else {
+ TransactionEntity t = new TransactionEntity(UUID.randomUUID().toString(), fromUser, toUser, dto.getAmount());
+ t.setNonce(dto.getNonce());
+
+ fromUser.setBalance(fromUser.getBalance() - dto.getAmount());
+ toUser.setBalance(toUser.getBalance() + dto.getAmount());
+
+ em.persist(t);
+ em.merge(fromUser);
+ em.merge(toUser);
+
+ res = new SocketResponse(SocketStatus.OK, SocketResponse.Message.TRANSACTION_SENT, t);
+ }
+
+ em.getTransaction().commit();
+ } catch (Exception e) {
+ if (em.getTransaction().isActive()) em.getTransaction().rollback();
+ logger.error(e.getMessage(), e);
+ res = new SocketResponse(SocketStatus.ERROR, SocketResponse.Message.UNKNOWN_ERROR, e.getMessage());
+ } finally {
+ em.close();
+ }
+ }
+
+ default -> {
+ logger.warn("Acción desconocida recibida: {}", msg.getAction());
+ res = new SocketResponse(SocketStatus.INVALID_REQUEST, SocketResponse.Message.UNKNOWN_ERROR);
+ }
+ }
+
+ output.println(gson.toJson(res));
+ } catch (Exception e) {
+ logger.error("Fallo procesando solicitud", e);
+ } finally {
+ input.close();
+ output.close();
+ socket.close();
+ }
+ }
+ }
+}
diff --git a/Integridos/src/main/resources/META-INF/persistence.xml b/Integridos/src/main/resources/META-INF/persistence.xml
new file mode 100644
index 0000000..2eed1d7
--- /dev/null
+++ b/Integridos/src/main/resources/META-INF/persistence.xml
@@ -0,0 +1,30 @@
+
+
+
+
+ Persistence unit for PAI-1
+
+
+ net.miarma.integridos.common.db.entities.UserEntity
+ net.miarma.integridos.common.db.entities.TransactionEntity
+ net.miarma.integridos.common.db.entities.SessionEntity
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Integridos/src/main/resources/default.properties b/Integridos/src/main/resources/default.properties
new file mode 100644
index 0000000..0e6879b
--- /dev/null
+++ b/Integridos/src/main/resources/default.properties
@@ -0,0 +1 @@
+secret=
\ No newline at end of file
diff --git a/Integridos/src/main/resources/images/banco.png b/Integridos/src/main/resources/images/banco.png
new file mode 100644
index 0000000..ebf2e60
Binary files /dev/null and b/Integridos/src/main/resources/images/banco.png differ
diff --git a/Integridos/src/main/resources/logback.xml b/Integridos/src/main/resources/logback.xml
new file mode 100644
index 0000000..afab6b9
--- /dev/null
+++ b/Integridos/src/main/resources/logback.xml
@@ -0,0 +1,53 @@
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n
+
+
+
+
+ log/client.log
+
+ %d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n
+
+
+ log/client-%d{yyyy-MM-dd}.log
+ 30
+ true
+
+
+
+
+ log/server.log
+
+ %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
+
+
+ log/server-%d{yyyy-MM-dd}.log
+ 30
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Integridos/src/main/resources/scripts/db.sql b/Integridos/src/main/resources/scripts/db.sql
new file mode 100644
index 0000000..243d967
--- /dev/null
+++ b/Integridos/src/main/resources/scripts/db.sql
@@ -0,0 +1,77 @@
+USE pai1;
+
+-- Cleanup
+DROP TABLE IF EXISTS sessions;
+DROP TABLE IF EXISTS transactions;
+DROP TABLE IF EXISTS users;
+-- END Cleanup
+
+-- DDL
+CREATE TABLE users (
+ userId UUID NOT NULL,
+ userName VARCHAR(64) NOT NULL UNIQUE,
+ password VARCHAR(256) NOT NULL,
+ balance DECIMAL(12,2) NOT NULL DEFAULT 0,
+ PRIMARY KEY (userId)
+);
+
+CREATE TABLE transactions (
+ transactionId UUID NOT NULL,
+ fromUser UUID NOT NULL,
+ toUser UUID NOT NULL,
+ transactionAmount DECIMAL(12,2) NOT NULL,
+ nonce TEXT NOT NULL,
+ PRIMARY KEY (transactionId),
+ FOREIGN KEY (fromUser) REFERENCES users (userId) ON DELETE CASCADE,
+ FOREIGN KEY (toUser) REFERENCES users (userId) ON DELETE CASCADE
+);
+
+CREATE TABLE sessions (
+ sessionId UUID NOT NULL,
+ userId UUID NOT NULL,
+ createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ expiresAt TIMESTAMP AS (createdAt + INTERVAL 15 MINUTE) STORED,
+ PRIMARY KEY (sessionId),
+ FOREIGN KEY (userId) REFERENCES users(userId) ON DELETE CASCADE
+);
+
+CREATE EVENT IF NOT EXISTS clear_expired_sessions
+ON SCHEDULE EVERY 1 MINUTE
+DO
+DELETE FROM sessions WHERE expiresAt <= NOW();
+
+CREATE OR REPLACE TRIGGER tr_subtract_amount
+AFTER INSERT ON transactions
+FOR EACH ROW
+BEGIN
+UPDATE users
+SET balance = balance - NEW.transactionAmount
+WHERE userId = NEW.fromUser;
+END;
+-- END DDL
+
+-- DML
+INSERT INTO users (userId, userName, password, balance)
+VALUES
+ (UUID(), 'pepe', 'pass123', 100.50),
+ (UUID(), 'lola', 'secreto', 250.00);
+
+INSERT INTO transactions (transactionId, fromUser, toUser, transactionAmount)
+VALUES (
+ UUID(),
+ (SELECT userId FROM users WHERE userName = 'pepe'),
+ (SELECT userId FROM users WHERE userName = 'lola'),
+ 25.75
+ );
+
+INSERT INTO sessions (sessionId, userId)
+VALUES (
+ UUID(),
+ (SELECT userId FROM users WHERE userName = 'pepe')
+ );
+-- END DML
+
+-- DQL
+SELECT * FROM sessions;
+
+-- END DQL
\ No newline at end of file
diff --git a/RedTeamPro/.gitkeep b/RedTeamPro/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Vulnaweb/.gitkeep b/Vulnaweb/.gitkeep
new file mode 100644
index 0000000..e69de29