From 746d0344610af017e89ddff6367b8a0b56cb9b0f Mon Sep 17 00:00:00 2001 From: Me Car Date: Sun, 10 Jan 2016 20:00:08 +0900 Subject: [PATCH] Implement a password reset flow. --- .../mage/client/dialog/ConnectDialog.form | 17 +- .../mage/client/dialog/ConnectDialog.java | 28 +- .../client/dialog/RegisterUserDialog.java | 11 +- .../client/dialog/ResetPasswordDialog.form | 279 +++++++++++++ .../client/dialog/ResetPasswordDialog.java | 393 ++++++++++++++++++ .../src/mage/interfaces/MageServer.java | 4 + Mage.Common/src/mage/remote/Connection.java | 9 + Mage.Common/src/mage/remote/SessionImpl.java | 40 ++ .../src/mage/remote/interfaces/Connect.java | 4 + .../main/java/mage/server/AuthorizedUser.java | 8 +- .../mage/server/AuthorizedUserRepository.java | 49 ++- .../main/java/mage/server/MageServerImpl.java | 66 ++- .../src/main/java/mage/server/Session.java | 20 +- .../main/java/mage/server/SessionManager.java | 9 + 14 files changed, 900 insertions(+), 37 deletions(-) create mode 100644 Mage.Client/src/main/java/mage/client/dialog/ResetPasswordDialog.form create mode 100644 Mage.Client/src/main/java/mage/client/dialog/ResetPasswordDialog.java diff --git a/Mage.Client/src/main/java/mage/client/dialog/ConnectDialog.form b/Mage.Client/src/main/java/mage/client/dialog/ConnectDialog.form index e64e95e503..d9d21a7e60 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/ConnectDialog.form +++ b/Mage.Client/src/main/java/mage/client/dialog/ConnectDialog.form @@ -59,11 +59,13 @@ - + + + @@ -115,10 +117,11 @@ + - - + + @@ -242,5 +245,13 @@ + + + + + + + + diff --git a/Mage.Client/src/main/java/mage/client/dialog/ConnectDialog.java b/Mage.Client/src/main/java/mage/client/dialog/ConnectDialog.java index 557e251042..d2a9c5e04b 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/ConnectDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/ConnectDialog.java @@ -77,6 +77,7 @@ public class ConnectDialog extends MageDialog { private Connection connection; private ConnectTask task; private RegisterUserDialog registerUserDialog; + private ResetPasswordDialog resetPasswordDialog; private final ActionListener connectAction = new ActionListener() { @Override @@ -98,6 +99,9 @@ public class ConnectDialog extends MageDialog { registerUserDialog = new RegisterUserDialog(); MageFrame.getDesktop().add(registerUserDialog, JLayeredPane.POPUP_LAYER); + + resetPasswordDialog = new ResetPasswordDialog(); + MageFrame.getDesktop().add(resetPasswordDialog, JLayeredPane.POPUP_LAYER); } public void showDialog() { @@ -157,6 +161,7 @@ public class ConnectDialog extends MageDialog { btnCancel = new javax.swing.JButton(); lblStatus = new javax.swing.JLabel(); btnRegister = new javax.swing.JButton(); + btnForgotPassword = new javax.swing.JButton(); setTitle("Connect to server"); setNormalBounds(new java.awt.Rectangle(100, 100, 410, 307)); @@ -237,6 +242,13 @@ public class ConnectDialog extends MageDialog { } }); + btnForgotPassword.setText("Forgot password"); + btnForgotPassword.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + btnForgotPasswordActionPerformed(evt); + } + }); + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); getContentPane().setLayout(layout); layout.setHorizontalGroup( @@ -269,11 +281,13 @@ public class ConnectDialog extends MageDialog { .addComponent(chkAutoConnect, javax.swing.GroupLayout.DEFAULT_SIZE, 375, Short.MAX_VALUE))) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() .addGap(0, 0, Short.MAX_VALUE) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) .addComponent(btnRegister) .addGroup(layout.createSequentialGroup() .addComponent(btnConnect) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(btnForgotPassword) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(btnCancel))) .addGap(26, 26, 26))) .addContainerGap()) @@ -313,10 +327,11 @@ public class ConnectDialog extends MageDialog { .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(btnConnect) - .addComponent(btnCancel)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(btnCancel) + .addComponent(btnForgotPassword)) + .addGap(3, 3, 3) .addComponent(btnRegister) - .addGap(3, 3, 3)) + .addContainerGap()) ); pack(); @@ -566,10 +581,15 @@ public class ConnectDialog extends MageDialog { registerUserDialog.showDialog(); }//GEN-LAST:event_btnRegisterActionPerformed + private void btnForgotPasswordActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnForgotPasswordActionPerformed + resetPasswordDialog.showDialog(); + }//GEN-LAST:event_btnForgotPasswordActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton btnCancel; private javax.swing.JButton btnConnect; private javax.swing.JButton btnFind; + private javax.swing.JButton btnForgotPassword; private javax.swing.JButton btnRegister; private mage.client.util.gui.countryBox.CountryComboBox cbFlag; private javax.swing.JCheckBox chkAutoConnect; diff --git a/Mage.Client/src/main/java/mage/client/dialog/RegisterUserDialog.java b/Mage.Client/src/main/java/mage/client/dialog/RegisterUserDialog.java index e6e989da65..c7c132a14c 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/RegisterUserDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/RegisterUserDialog.java @@ -234,16 +234,15 @@ public class RegisterUserDialog extends MageDialog { try { get(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS); if (result) { - lblStatus.setText("Registration succeeded"); - MageFrame.getInstance().showMessage("Registration succeeded"); + String message = "Registration succeeded"; + lblStatus.setText(message); + MageFrame.getInstance().showMessage(message); hideDialog(); } else { lblStatus.setText("Could not register"); } - } catch (InterruptedException ex) { - logger.fatal("Update Players Task error", ex); - } catch (ExecutionException ex) { - logger.fatal("Update Players Task error", ex); + } catch (InterruptedException | ExecutionException ex) { + logger.fatal("Registration task error", ex); } catch (CancellationException ex) { logger.info("Registration was canceled"); lblStatus.setText("Registration was canceled (but an account might have been actually created)"); diff --git a/Mage.Client/src/main/java/mage/client/dialog/ResetPasswordDialog.form b/Mage.Client/src/main/java/mage/client/dialog/ResetPasswordDialog.form new file mode 100644 index 0000000000..448116ad45 --- /dev/null +++ b/Mage.Client/src/main/java/mage/client/dialog/ResetPasswordDialog.form @@ -0,0 +1,279 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mage.Client/src/main/java/mage/client/dialog/ResetPasswordDialog.java b/Mage.Client/src/main/java/mage/client/dialog/ResetPasswordDialog.java new file mode 100644 index 0000000000..2d338884e3 --- /dev/null +++ b/Mage.Client/src/main/java/mage/client/dialog/ResetPasswordDialog.java @@ -0,0 +1,393 @@ +package mage.client.dialog; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import javax.swing.SwingWorker; +import mage.client.MageFrame; +import mage.client.util.Config; +import mage.remote.Connection; +import mage.remote.Session; +import mage.remote.SessionImpl; +import org.apache.log4j.Logger; + +public class ResetPasswordDialog extends MageDialog { + + private static final Logger logger = Logger.getLogger(ResetPasswordDialog.class); + private Connection connection; + private Session session; + private GetAuthTokenTask getAuthTokenTask; + private ResetPasswordTask resetPasswordTask; + + /** + * Creates new form ResetPasswordDialog + */ + public ResetPasswordDialog() { + initComponents(); + } + + public void showDialog() { + this.setModal(true); + this.setLocation(50, 50); + this.setVisible(true); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + jPanel2 = new javax.swing.JPanel(); + jLabel6 = new javax.swing.JLabel(); + lblAuthToken = new javax.swing.JLabel(); + lblPassword = new javax.swing.JLabel(); + lblPasswordConfirmation = new javax.swing.JLabel(); + txtAuthToken = new javax.swing.JTextField(); + btnSubmitNewPassword = new javax.swing.JButton(); + lblPasswordConfirmationReasoning = new javax.swing.JLabel(); + txtPassword = new javax.swing.JPasswordField(); + txtPasswordConfirmation = new javax.swing.JPasswordField(); + jPanel1 = new javax.swing.JPanel(); + jLabel5 = new javax.swing.JLabel(); + lblEmail = new javax.swing.JLabel(); + txtEmail = new javax.swing.JTextField(); + btnGetAuthToken = new javax.swing.JButton(); + lblStatus = new javax.swing.JLabel(); + btnCancel = new javax.swing.JButton(); + + setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); + setTitle("Reset password"); + + jPanel2.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + + jLabel6.setFont(new java.awt.Font("Lucida Grande", 1, 13)); // NOI18N + jLabel6.setText("Step 2:"); + + lblAuthToken.setLabelFor(txtAuthToken); + lblAuthToken.setText("Auth token:"); + + lblPassword.setLabelFor(txtPassword); + lblPassword.setText("New password:"); + + lblPasswordConfirmation.setLabelFor(txtPasswordConfirmation); + lblPasswordConfirmation.setText("New password:"); + + btnSubmitNewPassword.setText("Submit a new password"); + btnSubmitNewPassword.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + btnSubmitNewPasswordActionPerformed(evt); + } + }); + + lblPasswordConfirmationReasoning.setFont(new java.awt.Font("Lucida Grande", 0, 10)); // NOI18N + lblPasswordConfirmationReasoning.setLabelFor(txtPasswordConfirmation); + lblPasswordConfirmationReasoning.setText("(confirmation)"); + + javax.swing.GroupLayout jPanel2Layout = new javax.swing.GroupLayout(jPanel2); + jPanel2.setLayout(jPanel2Layout); + jPanel2Layout.setHorizontalGroup( + jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel2Layout.createSequentialGroup() + .addComponent(jLabel6) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(jPanel2Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel2Layout.createSequentialGroup() + .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) + .addComponent(lblAuthToken, javax.swing.GroupLayout.PREFERRED_SIZE, 74, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lblPassword, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(lblPasswordConfirmation, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(txtAuthToken) + .addComponent(txtPassword) + .addComponent(txtPasswordConfirmation))) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel2Layout.createSequentialGroup() + .addGap(0, 204, Short.MAX_VALUE) + .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(lblPasswordConfirmationReasoning, javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(btnSubmitNewPassword, javax.swing.GroupLayout.Alignment.TRAILING)))) + .addContainerGap()) + ); + jPanel2Layout.setVerticalGroup( + jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel2Layout.createSequentialGroup() + .addComponent(jLabel6) + .addGap(24, 24, 24) + .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(lblAuthToken) + .addComponent(txtAuthToken, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(lblPassword) + .addComponent(txtPassword, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(lblPasswordConfirmation) + .addComponent(txtPasswordConfirmation, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(lblPasswordConfirmationReasoning) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(btnSubmitNewPassword) + .addContainerGap(9, Short.MAX_VALUE)) + ); + + jPanel1.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + + jLabel5.setFont(new java.awt.Font("Lucida Grande", 1, 13)); // NOI18N + jLabel5.setText("Step 1:"); + + lblEmail.setLabelFor(txtEmail); + lblEmail.setText("Email:"); + + btnGetAuthToken.setText("Email an auth token"); + btnGetAuthToken.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + btnGetAuthTokenActionPerformed(evt); + } + }); + + javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1); + jPanel1.setLayout(jPanel1Layout); + jPanel1Layout.setHorizontalGroup( + jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel1Layout.createSequentialGroup() + .addComponent(jLabel5) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(jPanel1Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(jPanel1Layout.createSequentialGroup() + .addComponent(lblEmail) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtEmail)) + .addGroup(jPanel1Layout.createSequentialGroup() + .addGap(0, 0, Short.MAX_VALUE) + .addComponent(btnGetAuthToken))) + .addContainerGap()) + ); + jPanel1Layout.setVerticalGroup( + jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel1Layout.createSequentialGroup() + .addComponent(jLabel5) + .addGap(24, 24, 24) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(lblEmail) + .addComponent(txtEmail, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(btnGetAuthToken) + .addContainerGap()) + ); + + btnCancel.setText("Cancel"); + btnCancel.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + btnCancelActionPerformed(evt); + } + }); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGap(0, 0, Short.MAX_VALUE) + .addComponent(btnCancel)) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(jPanel2, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanel1, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(lblStatus, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(7, 7, 7) + .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(lblStatus, javax.swing.GroupLayout.PREFERRED_SIZE, 28, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(btnCancel) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + + pack(); + }// //GEN-END:initComponents + + private void btnGetAuthTokenActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnGetAuthTokenActionPerformed + if (this.txtEmail.getText().length() == 0) { + MageFrame.getInstance().showError("Please enter an email address."); + return; + } + + connection = new Connection(); + + // Use the default setting for server connection. + connection.setHost(MageFrame.getPreferences().get("serverAddress", Config.serverName)); + connection.setPort(Integer.valueOf(MageFrame.getPreferences().get("serverPort", Integer.toString(Config.port)))); + PreferencesDialog.setProxyInformation(connection); + + connection.setEmail(this.txtEmail.getText().trim()); + + getAuthTokenTask = new GetAuthTokenTask(); + getAuthTokenTask.execute(); + }//GEN-LAST:event_btnGetAuthTokenActionPerformed + + private void btnSubmitNewPasswordActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnSubmitNewPasswordActionPerformed + if (this.txtEmail.getText().length() == 0) { + MageFrame.getInstance().showError("Please enter an email address."); + return; + } + if (this.txtAuthToken.getText().length() == 0) { + MageFrame.getInstance().showError("Please enter an auth token."); + return; + } + if (this.txtPassword.getText().length() == 0) { + MageFrame.getInstance().showError("Please enter a new password."); + return; + } + if (!this.txtPassword.getText().equals(this.txtPasswordConfirmation.getText())) { + MageFrame.getInstance().showError("Passwords don't match."); + return; + } + + connection = new Connection(); + + // Use the default setting for server connection. + connection.setHost(MageFrame.getPreferences().get("serverAddress", Config.serverName)); + connection.setPort(Integer.valueOf(MageFrame.getPreferences().get("serverPort", Integer.toString(Config.port)))); + PreferencesDialog.setProxyInformation(connection); + + connection.setEmail(this.txtEmail.getText().trim()); + connection.setAuthToken(this.txtAuthToken.getText().trim()); + connection.setPassword(this.txtPassword.getText().trim()); + + resetPasswordTask = new ResetPasswordTask(); + resetPasswordTask.execute(); + }//GEN-LAST:event_btnSubmitNewPasswordActionPerformed + + private void btnCancelActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnCancelActionPerformed + this.hideDialog(); + }//GEN-LAST:event_btnCancelActionPerformed + + void disableButtons() { + btnGetAuthToken.setEnabled(false); + btnSubmitNewPassword.setEnabled(false); + } + + void enableButtons() { + btnGetAuthToken.setEnabled(true); + btnSubmitNewPassword.setEnabled(true); + } + + private class GetAuthTokenTask extends SwingWorker { + + private boolean result = false; + + private static final int CONNECTION_TIMEOUT_MS = 2100; + + @Override + protected Boolean doInBackground() throws Exception { + lblStatus.setText("Connecting..."); + disableButtons(); + session = new SessionImpl(MageFrame.getInstance()); + result = session.emailAuthToken(connection); + return result; + } + + @Override + protected void done() { + try { + get(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS); + if (result) { + String message = "Auth token is emailed. Please check your inbox."; + lblStatus.setText(message); + MageFrame.getInstance().showMessage(message); + } else { + lblStatus.setText("There was an issue while requesting an auth token."); + } + } catch (InterruptedException | ExecutionException ex) { + logger.fatal("Get Auth Token Task error", ex); + } catch (CancellationException ex) { + logger.info("Canceled"); + lblStatus.setText("Canceled"); + } catch (TimeoutException ex) { + logger.fatal("Timeout: ", ex); + } finally { + MageFrame.stopConnecting(); + enableButtons(); + } + } + } + + private class ResetPasswordTask extends SwingWorker { + + private boolean result = false; + + private static final int CONNECTION_TIMEOUT_MS = 2100; + + @Override + protected Boolean doInBackground() throws Exception { + lblStatus.setText("Connecting..."); + disableButtons(); + session = new SessionImpl(MageFrame.getInstance()); + result = session.resetPassword(connection); + return result; + } + + @Override + protected void done() { + try { + get(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS); + if (result) { + String message = "Password is reset successfully."; + lblStatus.setText(message); + MageFrame.getInstance().showMessage(message); + hideDialog(); + } else { + lblStatus.setText("There was an issue while resetting password."); + } + } catch (InterruptedException | ExecutionException ex) { + logger.fatal("Reset Password Task error", ex); + } catch (CancellationException ex) { + logger.info("Canceled"); + lblStatus.setText("Canceled"); + } catch (TimeoutException ex) { + logger.fatal("Timeout: ", ex); + } finally { + MageFrame.stopConnecting(); + enableButtons(); + } + } + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton btnCancel; + private javax.swing.JButton btnGetAuthToken; + private javax.swing.JButton btnSubmitNewPassword; + private javax.swing.JLabel jLabel5; + private javax.swing.JLabel jLabel6; + private javax.swing.JPanel jPanel1; + private javax.swing.JPanel jPanel2; + private javax.swing.JLabel lblAuthToken; + private javax.swing.JLabel lblEmail; + private javax.swing.JLabel lblPassword; + private javax.swing.JLabel lblPasswordConfirmation; + private javax.swing.JLabel lblPasswordConfirmationReasoning; + private javax.swing.JLabel lblStatus; + private javax.swing.JTextField txtAuthToken; + private javax.swing.JTextField txtEmail; + private javax.swing.JPasswordField txtPassword; + private javax.swing.JPasswordField txtPasswordConfirmation; + // End of variables declaration//GEN-END:variables +} diff --git a/Mage.Common/src/mage/interfaces/MageServer.java b/Mage.Common/src/mage/interfaces/MageServer.java index ac4dbf84ab..83ffce61e4 100644 --- a/Mage.Common/src/mage/interfaces/MageServer.java +++ b/Mage.Common/src/mage/interfaces/MageServer.java @@ -58,6 +58,10 @@ public interface MageServer { // registers a user to the user DB. boolean registerUser(String sessionId, String userName, String password, String email) throws MageException; + boolean emailAuthToken(String sessionId, String email) throws MageException; + + boolean resetPassword(String sessionId, String email, String authToken, String password) throws MageException; + // connection methods // DEPRECATED - Use connectUser instead. This is only kept for older clients. // This can be deleted once users transitioned to newer clients (1.4.6v1 and later). diff --git a/Mage.Common/src/mage/remote/Connection.java b/Mage.Common/src/mage/remote/Connection.java index 818ef0b46f..2a989da8dc 100644 --- a/Mage.Common/src/mage/remote/Connection.java +++ b/Mage.Common/src/mage/remote/Connection.java @@ -46,6 +46,7 @@ public class Connection { private String username; private String password; private String email; + private String authToken; private String adminPassword; private ProxyType proxyType; private String proxyHost; @@ -182,6 +183,14 @@ public class Connection { this.email = email; } + public String getAuthToken() { + return authToken; + } + + public void setAuthToken(String authToken) { + this.authToken = authToken; + } + public String getAdminPassword() { return adminPassword; } diff --git a/Mage.Common/src/mage/remote/SessionImpl.java b/Mage.Common/src/mage/remote/SessionImpl.java index 5a5d393218..4960fdfd74 100644 --- a/Mage.Common/src/mage/remote/SessionImpl.java +++ b/Mage.Common/src/mage/remote/SessionImpl.java @@ -204,6 +204,36 @@ public class SessionImpl implements Session { }); } + @Override + public synchronized boolean emailAuthToken(final Connection connection) { + return establishJBossRemotingConnection(connection) && handleRemotingTaskExceptions(new RemotingTask() { + @Override + public boolean run() throws Throwable { + logger.info("Trying to ask for an auth token to " + getEmail() + " to XMAGE server at " + connection.getHost() + ":" + connection.getPort()); + boolean result = server.emailAuthToken(sessionId, connection.getEmail()); + if (result) { + logger.info("An auth token is emailed to " + getEmail() + " from MAGE server at " + connection.getHost() + ":" + connection.getPort()); + } + return result; + } + }); + } + + @Override + public synchronized boolean resetPassword(final Connection connection) { + return establishJBossRemotingConnection(connection) && handleRemotingTaskExceptions(new RemotingTask() { + @Override + public boolean run() throws Throwable { + logger.info("Trying reset the password in XMAGE server at " + connection.getHost() + ":" + connection.getPort()); + boolean result = server.resetPassword(sessionId, connection.getEmail(), connection.getAuthToken(), connection.getPassword()); + if (result) { + logger.info("Password is successfully reset in MAGE server at " + connection.getHost() + ":" + connection.getPort()); + } + return result; + } + }); + } + @Override public synchronized boolean connect(final Connection connection) { return establishJBossRemotingConnection(connection) && handleRemotingTaskExceptions(new RemotingTask() { @@ -1445,6 +1475,16 @@ public class SessionImpl implements Session { return username == null ? "" : username; } + private String getEmail() { + String email = connection.getEmail(); + return email == null ? "" : email; + } + + private String getAuthToken() { + String authToken = connection.getAuthToken(); + return authToken == null ? "" : authToken; + } + @Override public boolean updatePreferencesForServer(UserData userData) { try { diff --git a/Mage.Common/src/mage/remote/interfaces/Connect.java b/Mage.Common/src/mage/remote/interfaces/Connect.java index 2b5921e2e7..3eeff3eebe 100644 --- a/Mage.Common/src/mage/remote/interfaces/Connect.java +++ b/Mage.Common/src/mage/remote/interfaces/Connect.java @@ -36,6 +36,10 @@ public interface Connect { boolean register(Connection connection); + boolean emailAuthToken(Connection connection); + + boolean resetPassword(Connection connection); + boolean connect(Connection connection); boolean stopConnecting(); diff --git a/Mage.Server/src/main/java/mage/server/AuthorizedUser.java b/Mage.Server/src/main/java/mage/server/AuthorizedUser.java index b9b754a698..f02b615e8c 100644 --- a/Mage.Server/src/main/java/mage/server/AuthorizedUser.java +++ b/Mage.Server/src/main/java/mage/server/AuthorizedUser.java @@ -14,7 +14,7 @@ import org.apache.shiro.crypto.hash.Hash; @DatabaseTable(tableName = "authorized_user") public class AuthorizedUser { - @DatabaseField(indexName = "name_index") + @DatabaseField(indexName = "name_index", unique = true) protected String name; @DatabaseField @@ -29,7 +29,7 @@ public class AuthorizedUser { @DatabaseField protected int hashIterations; - @DatabaseField + @DatabaseField(indexName = "email_index", unique = true) protected String email; public AuthorizedUser() { @@ -53,4 +53,8 @@ public class AuthorizedUser { ByteSource.Util.bytes(Base64.decode(this.salt)), ""); return matcher.doCredentialsMatch(token, info); } + + public String getName() { + return this.name; + } } diff --git a/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java b/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java index 3a56ec15a1..9507ce9a12 100644 --- a/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java +++ b/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java @@ -3,6 +3,7 @@ package mage.server; import com.j256.ormlite.dao.Dao; import com.j256.ormlite.dao.DaoManager; import com.j256.ormlite.jdbc.JdbcConnectionSource; +import com.j256.ormlite.stmt.DeleteBuilder; import com.j256.ormlite.stmt.QueryBuilder; import com.j256.ormlite.stmt.SelectArg; import com.j256.ormlite.support.ConnectionSource; @@ -11,7 +12,6 @@ import com.j256.ormlite.table.TableUtils; import java.io.File; import java.sql.SQLException; import java.util.List; -import java.util.concurrent.Callable; import mage.cards.repository.RepositoryUtil; import org.apache.log4j.Logger; import org.apache.shiro.crypto.RandomNumberGenerator; @@ -54,25 +54,25 @@ public enum AuthorizedUserRepository { public void add(final String userName, final String password, final String email) { try { - dao.callBatchTasks(new Callable() { - @Override - public Object call() throws Exception { - try { - Hash hash = new SimpleHash(Sha256Hash.ALGORITHM_NAME, password, rng.nextBytes(), 1024); - AuthorizedUser user = new AuthorizedUser(userName, hash, email); - dao.create(user); - } catch (SQLException ex) { - Logger.getLogger(AuthorizedUserRepository.class).error("Error adding a user to DB - ", ex); - } - return null; - } - }); - } catch (Exception ex) { - Logger.getLogger(AuthorizedUserRepository.class).error("Error adding a authorized_user - ", ex); + Hash hash = new SimpleHash(Sha256Hash.ALGORITHM_NAME, password, rng.nextBytes(), 1024); + AuthorizedUser user = new AuthorizedUser(userName, hash, email); + dao.create(user); + } catch (SQLException ex) { + Logger.getLogger(AuthorizedUserRepository.class).error("Error adding a user to DB - ", ex); } } - public AuthorizedUser get(String userName) { + public void remove(final String userName) { + try { + DeleteBuilder db = dao.deleteBuilder(); + db.where().eq("name", new SelectArg(userName)); + db.delete(); + } catch (SQLException ex) { + Logger.getLogger(AuthorizedUserRepository.class).error("Error removing a user from DB - ", ex); + } + } + + public AuthorizedUser getByName(String userName) { try { QueryBuilder qb = dao.queryBuilder(); qb.where().eq("name", new SelectArg(userName)); @@ -87,6 +87,21 @@ public enum AuthorizedUserRepository { return null; } + public AuthorizedUser getByEmail(String userName) { + try { + QueryBuilder qb = dao.queryBuilder(); + qb.where().eq("email", new SelectArg(userName)); + List results = dao.query(qb.prepare()); + if (results.size() == 1) { + return results.get(0); + } + return null; + } catch (SQLException ex) { + Logger.getLogger(AuthorizedUserRepository.class).error("Error getting a authorized_user - ", ex); + } + return null; + } + public void closeDB() { try { if (dao != null && dao.getConnectionSource() != null) { diff --git a/Mage.Server/src/main/java/mage/server/MageServerImpl.java b/Mage.Server/src/main/java/mage/server/MageServerImpl.java index 2ec1ad0aec..2059324b70 100644 --- a/Mage.Server/src/main/java/mage/server/MageServerImpl.java +++ b/Mage.Server/src/main/java/mage/server/MageServerImpl.java @@ -27,9 +27,12 @@ */ package mage.server; +import java.security.SecureRandom; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; @@ -95,9 +98,17 @@ public class MageServerImpl implements MageServer { private static final Logger logger = Logger.getLogger(MageServerImpl.class); private static final ExecutorService callExecutor = ThreadExecutor.getInstance().getCallExecutor(); - + private static final SecureRandom RANDOM = new SecureRandom(); + private final String adminPassword; private final boolean testMode; + private final LinkedHashMap activeAuthTokens = new LinkedHashMap() { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + // Keep the latest 1024 auth tokens in memory. + return size() > 1024; + } + }; public MageServerImpl(String adminPassword, boolean testMode) { this.adminPassword = adminPassword; @@ -110,6 +121,50 @@ public class MageServerImpl implements MageServer { return SessionManager.getInstance().registerUser(sessionId, userName, password, email); } + // generateAuthToken returns a uniformly distributed 6-digits string. + static private String generateAuthToken() { + return String.format("%06d", RANDOM.nextInt(1000000)); + } + + @Override + public boolean emailAuthToken(String sessionId, String email) throws MageException { + AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.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); + if (!GmailClient.sendMessage(email, "XMage Password Reset Auth Token", + "Use this auth token to reset your password: " + authToken + "\n" + + "It's valid until the next server restart.")) { + sendErrorMessageToClient(sessionId, "There was an error inside the server while emailing an auth token"); + return false; + } + return true; + } + + @Override + public boolean resetPassword(String sessionId, String email, String authToken, String password) throws MageException { + 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); + if (authorizedUser == null) { + sendErrorMessageToClient(sessionId, "The user is no longer in the DB"); + 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); + activeAuthTokens.remove(email); + return true; + } + @Override public boolean registerClient(String userName, String sessionId, MageVersion version) throws MageException { // This method is deprecated, so just inform the server version. @@ -1045,6 +1100,15 @@ public class MageServerImpl implements MageServer { } } + private void sendErrorMessageToClient(final String sessionId, final String message) throws MageException { + execute("sendErrorMessageToClient", sessionId, new Action() { + @Override + public void execute() { + SessionManager.getInstance().sendErrorMessageToClient(sessionId, message); + } + }); + } + protected void execute(final String actionName, final String sessionId, final Action action, boolean checkAdminRights) throws MageException { if (checkAdminRights) { if (!SessionManager.getInstance().isAdmin(sessionId)) { diff --git a/Mage.Server/src/main/java/mage/server/Session.java b/Mage.Server/src/main/java/mage/server/Session.java index 891acc7283..279d08ed40 100644 --- a/Mage.Server/src/main/java/mage/server/Session.java +++ b/Mage.Server/src/main/java/mage/server/Session.java @@ -95,6 +95,11 @@ public class Session { sendErrorMessageToClient(returnMessage); return returnMessage; } + returnMessage = validateEmail(email); + if (returnMessage != null) { + sendErrorMessageToClient(returnMessage); + return returnMessage; + } AuthorizedUserRepository.instance.add(userName, password, email); if (GmailClient.sendMessage(email, "XMage Registration Completed", "You are successfully registered as " + userName + ".")) { @@ -121,7 +126,7 @@ public class Session { if (m.find()) { return "User name '" + userName + "' includes not allowed characters: use a-z, A-Z and 0-9"; } - AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.get(userName); + AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByName(userName); if (authorizedUser != null) { return "User name '" + userName + "' already in use"; } @@ -147,6 +152,14 @@ public class Session { return null; } + static private String validateEmail(String email) { + AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByEmail(email); + if (authorizedUser != null) { + return "Email address '" + email + "' is associated with another user"; + } + return null; + } + public String connectUser(String userName, String password) throws MageException { String returnMessage = connectUserHandling(userName, password); if (returnMessage != null) { @@ -161,9 +174,8 @@ public class Session { public String connectUserHandling(String userName, String password) throws MageException { this.isAdmin = false; - if (ConfigSettings.getInstance().isAuthenticationActivated()) { - AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.get(userName); + AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByName(userName); if (authorizedUser == null || !authorizedUser.doCredentialsMatch(userName, password)) { return "Wrong username or password"; } @@ -347,7 +359,7 @@ public class Session { this.host = hostAddress; } - void sendErrorMessageToClient(String message) { + public void sendErrorMessageToClient(String message) { List messageData = new LinkedList<>(); messageData.add("Error while connecting to server"); messageData.add(message); diff --git a/Mage.Server/src/main/java/mage/server/SessionManager.java b/Mage.Server/src/main/java/mage/server/SessionManager.java index 63ef1ab914..f52a138a4e 100644 --- a/Mage.Server/src/main/java/mage/server/SessionManager.java +++ b/Mage.Server/src/main/java/mage/server/SessionManager.java @@ -237,4 +237,13 @@ public class SessionManager { } return false; } + + public void sendErrorMessageToClient(String sessionId, String message) { + Session session = sessions.get(sessionId); + if (session == null) { + logger.error("Following error message is not delivered because session " + sessionId + " is not found: " + message); + return; + } + session.sendErrorMessageToClient(message); + } }