package service; import client.Client; import client.DefaultDates; import client.LoginState; import database.account.Account; import database.account.AccountRepository; import lombok.extern.slf4j.Slf4j; import net.server.Server; import net.server.coordinator.session.Hwid; import net.server.coordinator.session.SessionCoordinator; import server.TimerManager; import tools.BCrypt; import tools.DatabaseConnection; import java.sql.Connection; import java.sql.Date; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.time.Instant; import java.time.LocalDate; import java.util.Optional; /** * @author Ponk */ @Slf4j public class AccountService { private static final LocalDate GMS_RELEASE = LocalDate.of(2005, 5, 11); private static final byte INITIAL_CHR_SLOTS = 3; private final AccountRepository accountRepository; public AccountService(AccountRepository accountRepository) { this.accountRepository = accountRepository; } public Account createAccount(String name, String password) { Account account = createAccountPostgres(name, password); createAccountMysql(account.id(), name, password); return account; } private Account createAccountPostgres(String name, String password) { Account newAccount = Account.builder() .name(name) .password(hashPassword(password)) .birthdate(GMS_RELEASE) .chrSlots(INITIAL_CHR_SLOTS) .loginState(LoginState.LOGGED_OUT) .gender(null) .build(); Integer accountId; try { accountId = accountRepository.insert(newAccount); } catch (Exception e) { log.error("Failed to insert new account", e); throw new RuntimeException("Failed to insert new account"); } return getAccount(accountId) .orElseThrow(() -> new RuntimeException("Failed to get account after insert, id: " + accountId)); } private void createAccountMysql(int id, String name, String password) { try (Connection con = DatabaseConnection.getConnection(); PreparedStatement ps = con.prepareStatement("INSERT INTO accounts (id, name, password, birthday, tempban) VALUES (?, ?, ?, ?, ?);")) { ps.setInt(1, id); ps.setString(2, name); ps.setString(3, BCrypt.hashpw(password, BCrypt.gensalt())); ps.setDate(4, Date.valueOf(DefaultDates.getBirthday())); ps.setTimestamp(5, Timestamp.valueOf(DefaultDates.getTempban())); ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(e); } } private String hashPassword(String password) { return BCrypt.hashpw(password, BCrypt.gensalt()); } public Optional getAccount(String name) { return accountRepository.findByNameIgnoreCase(name); } public Optional getAccount(int accountId) { return accountRepository.findById(accountId); } public Optional getAccountIdByChrName(String chrName) { return accountRepository.findIdByChrNameIgnoreCase(chrName); } public boolean acceptTos(int accountId) { acceptTosMysql(accountId); acceptTosPostgres(accountId); return true; } private boolean acceptTosMysql(int accountId) { try (Connection con = DatabaseConnection.getConnection()) { try (PreparedStatement ps = con.prepareStatement("SELECT `tos` FROM accounts WHERE id = ?")) { ps.setInt(1, accountId); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { if (rs.getByte("tos") == 1) { return false; } } } } try (PreparedStatement ps = con.prepareStatement("UPDATE accounts SET tos = 1 WHERE id = ?")) { ps.setInt(1, accountId); ps.executeUpdate(); } } catch (SQLException e) { e.printStackTrace(); } return true; } private boolean acceptTosPostgres(int accountId) { Optional account = getAccount(accountId); if (account.isEmpty()) { return false; } if (account.get().acceptedTos()) { return false; } accountRepository.setTos(accountId, true); return true; } public boolean setGender(int accountId, byte gender) { setGenderMysql(accountId, gender); return setGenderPostgres(accountId, gender); } private void setGenderMysql(int accountId, byte gender) { try (Connection con = DatabaseConnection.getConnection(); PreparedStatement ps = con.prepareStatement("UPDATE accounts SET gender = ? WHERE id = ?")) { ps.setByte(1, gender); ps.setInt(2, accountId); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } } private boolean setGenderPostgres(int accountId, byte gender) { boolean success = accountRepository.setGender(accountId, gender); if (!success) { log.warn("Failed to set gender, account:{}, gender:{}", accountId, gender); } return success; } public void setPin(int accountId, String pin) { setPinMysql(accountId, pin); setPinPostgres(accountId, pin); } private void setPinMysql(int accountId, String pin) { try (Connection con = DatabaseConnection.getConnection(); PreparedStatement ps = con.prepareStatement("UPDATE accounts SET pin = ? WHERE id = ?")) { ps.setString(1, pin); ps.setInt(2, accountId); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } } private void setPinPostgres(int accountId, String pin) { try { boolean success = accountRepository.setPin(accountId, pin); if (!success) { log.warn("Failed to set pin (no updated rows) - account:{}, pin:{}", accountId, pin); } } catch (Exception e) { log.error("Failed to set pin due to error - account:{}, pin:{}", accountId, pin, e); } } public void setPic(int accountId, String pic) { setPicMysql(accountId, pic); setPicPostgres(accountId, pic); } private void setPicMysql(int accountId, String pic) { try (Connection con = DatabaseConnection.getConnection(); PreparedStatement ps = con.prepareStatement("UPDATE accounts SET pic = ? WHERE id = ?")) { ps.setString(1, pic); ps.setInt(2, accountId); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } } private void setPicPostgres(int accountId, String pic) { try { boolean success = accountRepository.setPic(accountId, pic); if (!success) { log.warn("Failed to set pic (no updated rows) - account:{}, pic:{}", accountId, pic); } } catch (Exception e) { log.error("Failed to set pic - account:{}, pin:{}", accountId, pic, e); } } public boolean addChrSlot(Client c) { if (!c.gainCharacterSlot()) { return false; } int newChrSlots = c.getCharacterSlots() + 1; setChrSlotsMysql(c.getAccID(), newChrSlots); return setChrSlotsPostgres(c.getAccID(), newChrSlots); } private void setChrSlotsMysql(int accountId, int chrSlots) { try (Connection con = DatabaseConnection.getConnection(); PreparedStatement ps = con.prepareStatement("UPDATE accounts SET characterslots = ? WHERE id = ?")) { ps.setInt(1, chrSlots); ps.setInt(2, accountId); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } } private boolean setChrSlotsPostgres(int accountId, int chrSlots) { return accountRepository.setChrSlots(accountId, chrSlots); } public boolean setLoggedIn(Client c) { Account account = c.getAccount(); if (account == null) { throw new IllegalStateException("Unable to set logged in - no account"); } LoginState currentState = account.loginState(); if (currentState != LoginState.LOGGED_OUT && currentState != LoginState.SERVER_TRANSITION) { return false; } setLoginState(c, LoginState.LOGGED_IN); return true; } public void setLoggedOutAndDisconnect(Client c) { SessionCoordinator.getInstance().closeSession(c, false); setLoggedOut(c); } // TODO: check "stuck" accounts periodically and log them out. public void setLoggedOut(Client c) { setLoginState(c, LoginState.LOGGED_OUT); } public void setInTransition(Client c) { setLoginState(c, LoginState.SERVER_TRANSITION); } private void setLoginState(Client c, LoginState newState) { saveLoginState(c.getAccID(), newState); c.onChangedLoginState(newState); } private void saveLoginState(int accountId, LoginState newState) { setLoginStateMysql(accountId, newState); setLoginStatePostgres(accountId, newState); } private void setLoginStateMysql(int accountId, LoginState newState) { try (Connection con = DatabaseConnection.getConnection(); PreparedStatement ps = con.prepareStatement("UPDATE accounts SET loggedin = ?, lastlogin = ? WHERE id = ?")) { // using sql currenttime here could potentially break the login, thanks Arnah for pointing this out ps.setInt(1, newState.getValue()); ps.setTimestamp(2, new java.sql.Timestamp(Server.getInstance().getCurrentTime())); ps.setInt(3, accountId); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } } private void setLoginStatePostgres(int accountId, LoginState newState) { Instant loginTime = Instant.now(); boolean success = accountRepository.setLoginState(accountId, newState, loginTime); if (!success) { log.warn("Failed to set login state - account:{}, newState:{}, loginTime:{}", accountId, newState, loginTime); } } public void setIpAndMacsAndHwidAsync(int accountId, final String ip, final String macs, Hwid hwid) { final String hwidToSave = hwid != null ? hwid.hwid() : null; TimerManager.getInstance().schedule(() -> { try { accountRepository.setIpAndMacsAndHwid(accountId, ip, hwidToSave, macs); } catch (Exception e) { log.error("Failed to save ip: {}, macs: {}, hwid: {} for accountId: {}", ip, hwidToSave, macs, accountId, e); } }, 0); } public boolean ban(int accountId, Instant bannedUntil, byte banReason, String description) { return accountRepository.setBanned(accountId, bannedUntil, banReason, description); } }