Server improves:

* Server: improved messages on register/reset dialogs;
* Tests: added database compatible tests on new code or libs (auth db);
This commit is contained in:
Oleg Agafonov 2021-10-01 21:52:09 +04:00
parent ec87af8d9a
commit 301539d75b
8 changed files with 138 additions and 30 deletions

View file

@ -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;
}
});

View file

@ -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<AuthorizedUser, Object> 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);

View file

@ -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<User> u = managerFactory.userManager().getUserByName(userName);
if (u.isPresent()) {
User user = u.get();

View file

@ -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;
}

View file

@ -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)
);
}
}
}

View file

@ -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);
}
}
}

Binary file not shown.

View file

@ -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() {
}
}