diff --git a/Mage.Common/src/main/java/mage/remote/SessionImpl.java b/Mage.Common/src/main/java/mage/remote/SessionImpl.java index 3162aa68e6..c8b3f7f1a3 100644 --- a/Mage.Common/src/main/java/mage/remote/SessionImpl.java +++ b/Mage.Common/src/main/java/mage/remote/SessionImpl.java @@ -232,7 +232,7 @@ public class SessionImpl implements Session { public boolean work() throws Throwable { logger.info("Password reset: reseting password for username " + getUserName()); boolean result = server.resetPassword(sessionId, connection.getEmail(), connection.getAuthToken(), connection.getPassword()); - logger.info("Password reset: " + (result ? "DONE, check your email for new password" : "FAIL")); + logger.info("Password reset: " + (result ? "DONE, now you can login with new password" : "FAIL")); return result; } }); diff --git a/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java b/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java index f1915e72f1..2f817d7db0 100644 --- a/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java +++ b/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java @@ -22,9 +22,7 @@ import java.io.File; import java.sql.SQLException; import java.util.List; -public enum AuthorizedUserRepository { - - instance; +public class AuthorizedUserRepository { private static final String JDBC_URL = "jdbc:h2:file:./db/authorized_user.h2;AUTO_SERVER=TRUE"; private static final String VERSION_ENTITY_NAME = "authorized_user"; @@ -32,15 +30,20 @@ public enum AuthorizedUserRepository { private static final long DB_VERSION = 2; private static final RandomNumberGenerator rng = new SecureRandomNumberGenerator(); + private static final AuthorizedUserRepository instance; + static { + instance = new AuthorizedUserRepository(JDBC_URL); + } + private Dao dao; - AuthorizedUserRepository() { + public AuthorizedUserRepository(String connectionString) { File file = new File("db"); if (!file.exists()) { file.mkdirs(); } try { - ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL); + ConnectionSource connectionSource = new JdbcConnectionSource(connectionString); TableUtils.createTableIfNotExists(connectionSource, AuthorizedUser.class); dao = DaoManager.createDao(connectionSource, AuthorizedUser.class); } catch (SQLException ex) { @@ -48,6 +51,10 @@ public enum AuthorizedUserRepository { } } + public static AuthorizedUserRepository getInstance() { + return instance; + } + public void add(final String userName, final String password, final String email) { try { Hash hash = new SimpleHash(Sha256Hash.ALGORITHM_NAME, password, rng.nextBytes(), 1024); diff --git a/Mage.Server/src/main/java/mage/server/MageServerImpl.java b/Mage.Server/src/main/java/mage/server/MageServerImpl.java index 0992ab6cc0..6fa1a21a88 100644 --- a/Mage.Server/src/main/java/mage/server/MageServerImpl.java +++ b/Mage.Server/src/main/java/mage/server/MageServerImpl.java @@ -83,15 +83,17 @@ public class MageServerImpl implements MageServer { @Override public boolean emailAuthToken(String sessionId, String email) throws MageException { if (!managerFactory.configSettings().isAuthenticationActivated()) { - sendErrorMessageToClient(sessionId, "Registration is disabled by the server config"); + sendErrorMessageToClient(sessionId, Session.REGISTRATION_DISABLED_MESSAGE); return false; } - AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByEmail(email); + + AuthorizedUser authorizedUser = AuthorizedUserRepository.getInstance().getByEmail(email); if (authorizedUser == null) { sendErrorMessageToClient(sessionId, "No user was found with the email address " + email); logger.info("Auth token is requested for " + email + " but there's no such user in DB"); return false; } + String authToken = generateAuthToken(); activeAuthTokens.put(email, authToken); String subject = "XMage Password Reset Auth Token"; @@ -113,23 +115,31 @@ public class MageServerImpl implements MageServer { @Override public boolean resetPassword(String sessionId, String email, String authToken, String password) throws MageException { if (!managerFactory.configSettings().isAuthenticationActivated()) { - sendErrorMessageToClient(sessionId, "Registration is disabled by the server config"); + sendErrorMessageToClient(sessionId, Session.REGISTRATION_DISABLED_MESSAGE); return false; } + + // multi-step reset: + // - send auth token + // - check auth token to confirm reset + String storedAuthToken = activeAuthTokens.get(email); if (storedAuthToken == null || !storedAuthToken.equals(authToken)) { sendErrorMessageToClient(sessionId, "Invalid auth token"); logger.info("Invalid auth token " + authToken + " is sent for " + email); return false; } - AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByEmail(email); + + AuthorizedUser authorizedUser = AuthorizedUserRepository.getInstance().getByEmail(email); if (authorizedUser == null) { - sendErrorMessageToClient(sessionId, "The user is no longer in the DB"); + sendErrorMessageToClient(sessionId, "User with that email doesn't exists"); logger.info("Auth token is valid, but the user with email address " + email + " is no longer in the DB"); return false; } - AuthorizedUserRepository.instance.remove(authorizedUser.getName()); - AuthorizedUserRepository.instance.add(authorizedUser.getName(), password, email); + + // recreate user with new password + AuthorizedUserRepository.getInstance().remove(authorizedUser.getName()); + AuthorizedUserRepository.getInstance().add(authorizedUser.getName(), password, email); activeAuthTokens.remove(email); return true; } @@ -1042,7 +1052,7 @@ public class MageServerImpl implements MageServer { @Override public void setActivation(final String sessionId, final String userName, boolean active) throws MageException { execute("setActivation", sessionId, () -> { - AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByName(userName); + AuthorizedUser authorizedUser = AuthorizedUserRepository.getInstance().getByName(userName); Optional u = managerFactory.userManager().getUserByName(userName); if (u.isPresent()) { User user = u.get(); diff --git a/Mage.Server/src/main/java/mage/server/Main.java b/Mage.Server/src/main/java/mage/server/Main.java index e78807b162..2c967fb7d0 100644 --- a/Mage.Server/src/main/java/mage/server/Main.java +++ b/Mage.Server/src/main/java/mage/server/Main.java @@ -66,7 +66,15 @@ public final class Main { public static final PluginClassLoader classLoader = new PluginClassLoader(); private static TransporterServer server; + + // special test mode: + // - fast game buttons; + // - cheat commands; + // - no deck validation; + // - simplified registration and login (no password check); + // - debug main menu for GUI and rendering testing; private static boolean testMode; + private static boolean fastDbMode; /** @@ -98,7 +106,7 @@ public final class Main { if (config.isAuthenticationActivated()) { logger.info("Check authorized user DB version ..."); - if (!AuthorizedUserRepository.instance.checkAlterAndMigrateAuthorizedUser()) { + if (!AuthorizedUserRepository.getInstance().checkAlterAndMigrateAuthorizedUser()) { logger.fatal("Failed to start server."); return; } diff --git a/Mage.Server/src/main/java/mage/server/Session.java b/Mage.Server/src/main/java/mage/server/Session.java index 237a27ec62..4d1dde28ad 100644 --- a/Mage.Server/src/main/java/mage/server/Session.java +++ b/Mage.Server/src/main/java/mage/server/Session.java @@ -35,6 +35,8 @@ public class Session { private static final Pattern alphabetsPattern = Pattern.compile("[a-zA-Z]"); private static final Pattern digitsPattern = Pattern.compile("[0-9]"); + public static final String REGISTRATION_DISABLED_MESSAGE = "Registration has been disabled on the server. You can use any name and empty password to login."; + private final ManagerFactory managerFactory; private final String sessionId; private UUID userId; @@ -60,30 +62,37 @@ public class Session { public String registerUser(String userName, String password, String email) throws MageException { if (!managerFactory.configSettings().isAuthenticationActivated()) { - String returnMessage = "Registration is disabled by the server config"; + String returnMessage = REGISTRATION_DISABLED_MESSAGE; sendErrorMessageToClient(returnMessage); return returnMessage; } - synchronized (AuthorizedUserRepository.instance) { + synchronized (AuthorizedUserRepository.getInstance()) { + // name String returnMessage = validateUserName(userName); if (returnMessage != null) { sendErrorMessageToClient(returnMessage); return returnMessage; } + // auto-generated password RandomString randomString = new RandomString(10); password = randomString.nextString(); returnMessage = validatePassword(password, userName); if (returnMessage != null) { - sendErrorMessageToClient(returnMessage); + logger.warn("pas: " + password); + sendErrorMessageToClient("Auto-generated password fail, try again: " + returnMessage); return returnMessage; } + + // email returnMessage = validateEmail(email); if (returnMessage != null) { sendErrorMessageToClient(returnMessage); return returnMessage; } - AuthorizedUserRepository.instance.add(userName, password, email); + + // create + AuthorizedUserRepository.getInstance().add(userName, password, email); String text = "You are successfully registered as " + userName + '.'; text += " Your initial, generated password is: " + password; @@ -95,15 +104,15 @@ public class Session { success = managerFactory.mailgunClient().sendMessage(email, subject, text); } if (success) { - String ok = "Sent a registration confirmation / initial password email to " + email + " for " + userName; + String ok = "Email with initial password sent to " + email + " for a user " + userName; logger.info(ok); sendInfoMessageToClient(ok); } else if (Main.isTestMode()) { - String ok = "Server is in test mode. Your account is registered with a password of " + password + " for " + userName; + String ok = "Email sending failed. Server is in test mode. Your account registered with a password " + password + " for a user " + userName; logger.info(ok); sendInfoMessageToClient(ok); } else { - String err = "Failed sending a registration confirmation / initial password email to " + email + " for " + userName; + String err = "Email sending failed. Try use another email address or service. Or reset password by email " + email + " for a user " + userName; logger.error(err); sendErrorMessageToClient(err); return err; @@ -113,9 +122,13 @@ public class Session { } private String validateUserName(String userName) { + // return error message or null on good name + if (userName.equals("Admin")) { + // virtual user for admin console return "User name Admin already in use"; } + ConfigSettings config = managerFactory.configSettings(); if (userName.length() < config.getMinUserNameLength()) { return "User name may not be shorter than " + config.getMinUserNameLength() + " characters"; @@ -123,15 +136,19 @@ public class Session { if (userName.length() > config.getMaxUserNameLength()) { return "User name may not be longer than " + config.getMaxUserNameLength() + " characters"; } + Pattern invalidUserNamePattern = Pattern.compile(managerFactory.configSettings().getInvalidUserNamePattern(), Pattern.CASE_INSENSITIVE); Matcher m = invalidUserNamePattern.matcher(userName); if (m.find()) { return "User name '" + userName + "' includes not allowed characters: use a-z, A-Z and 0-9"; } - AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByName(userName); + + AuthorizedUser authorizedUser = AuthorizedUserRepository.getInstance().getByName(userName); if (authorizedUser != null) { return "User name '" + userName + "' already in use"; } + + // all fine return null; } @@ -159,7 +176,7 @@ public class Session { if (email == null || email.isEmpty()) { return "Email address cannot be blank"; } - AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByEmail(email); + AuthorizedUser authorizedUser = AuthorizedUserRepository.getInstance().getByEmail(email); if (authorizedUser != null) { return "Email address '" + email + "' is associated with another user"; } @@ -182,8 +199,8 @@ public class Session { this.isAdmin = false; AuthorizedUser authorizedUser = null; if (managerFactory.configSettings().isAuthenticationActivated()) { - authorizedUser = AuthorizedUserRepository.instance.getByName(userName); - String errorMsg = "Wrong username or password. In case you haven't, please register your account first."; + authorizedUser = AuthorizedUserRepository.getInstance().getByName(userName); + String errorMsg = "Wrong username or password. You must register your account first."; if (authorizedUser == null) { return errorMsg; } @@ -193,16 +210,16 @@ public class Session { } if (!authorizedUser.active) { - return "Your profile is deactivated, you can't sign on."; + return "Your profile has been deactivated by admin."; } if (authorizedUser.lockedUntil != null) { if (authorizedUser.lockedUntil.compareTo(Calendar.getInstance().getTime()) > 0) { - return "Your profile is deactivated until " + SystemUtil.dateFormat.format(authorizedUser.lockedUntil); + return "Your profile has need deactivated by admin until " + SystemUtil.dateFormat.format(authorizedUser.lockedUntil); } else { + // unlock on timeout end managerFactory.userManager().createUser(userName, host, authorizedUser).ifPresent(user -> user.setLockedUntil(null) ); - } } } diff --git a/Mage.Server/src/main/java/mage/server/User.java b/Mage.Server/src/main/java/mage/server/User.java index bf7f025da4..41e0378c96 100644 --- a/Mage.Server/src/main/java/mage/server/User.java +++ b/Mage.Server/src/main/java/mage/server/User.java @@ -817,7 +817,7 @@ public class User { authorizedUser.chatLockedUntil = this.chatLockedUntil; authorizedUser.lockedUntil = this.lockedUntil; authorizedUser.active = this.active; - AuthorizedUserRepository.instance.update(authorizedUser); + AuthorizedUserRepository.getInstance().update(authorizedUser); } } } diff --git a/Mage.Tests/src/test/data/users-db-sample.h2.mv.db b/Mage.Tests/src/test/data/users-db-sample.h2.mv.db new file mode 100644 index 0000000000..91b77c4d56 Binary files /dev/null and b/Mage.Tests/src/test/data/users-db-sample.h2.mv.db differ diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/DatabaseCompatibleTest.java b/Mage.Tests/src/test/java/org/mage/test/serverside/DatabaseCompatibleTest.java new file mode 100644 index 0000000000..4066f45ac7 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/DatabaseCompatibleTest.java @@ -0,0 +1,66 @@ +package org.mage.test.serverside; + +import mage.server.AuthorizedUser; +import mage.server.AuthorizedUserRepository; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * Testing database compatible on new libs or updates. + * + * @author JayDi85 + */ +public class DatabaseCompatibleTest { + + private final String JDBC_URL = "jdbc:h2:file:%s;AUTO_SERVER=TRUE"; + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void test_AuthUsers() { + try { + // prepare test db + String dbDir = tempFolder.newFolder().getAbsolutePath(); + String dbName = "users-db-sample.h2"; + String dbFullName = Paths.get(dbDir, dbName).toAbsolutePath().toString(); + String dbFullFileName = dbFullName + ".mv.db"; + Files.copy( + Paths.get("src", "test", "data", dbName + ".mv.db"), + Paths.get(dbFullFileName) + ); + Assert.assertTrue(Files.exists(Paths.get(dbFullFileName))); + + AuthorizedUserRepository dbUsers = new AuthorizedUserRepository( + String.format(JDBC_URL, dbFullName) + ); + + // search + Assert.assertNotNull(dbUsers.getByName("user1")); + Assert.assertNotNull(dbUsers.getByEmail("user2@example.com")); + Assert.assertNull(dbUsers.getByName("userFAIL")); + + // login + AuthorizedUser user = dbUsers.getByName("user3"); + Assert.assertEquals("user name", user.getName(), "user3"); + Assert.assertTrue("user pas", user.doCredentialsMatch("user3", "pas3")); + Assert.assertFalse("user wrong pas", user.doCredentialsMatch("user3", "123")); + Assert.assertFalse("user empty pas", user.doCredentialsMatch("user3", "")); + } catch (IOException e) { + e.printStackTrace(); + Assert.fail(e.getMessage()); + } + } + + @Test + @Ignore // TODO: add records/stats db compatible test + public void test_Records() { + } +}