diff --git a/client/src/com/mirth/connect/client/ui/LoginPanel.form b/client/src/com/mirth/connect/client/ui/DefaultLoginPanel.form similarity index 100% rename from client/src/com/mirth/connect/client/ui/LoginPanel.form rename to client/src/com/mirth/connect/client/ui/DefaultLoginPanel.form diff --git a/client/src/com/mirth/connect/client/ui/DefaultLoginPanel.java b/client/src/com/mirth/connect/client/ui/DefaultLoginPanel.java new file mode 100644 index 0000000000..98c8905d2d --- /dev/null +++ b/client/src/com/mirth/connect/client/ui/DefaultLoginPanel.java @@ -0,0 +1,575 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: Mirth Corporation +// SPDX-FileCopyrightText: 2025 Mitch Gaffigan and Tony Germano + +package com.mirth.connect.client.ui; + +import java.awt.Color; +import java.awt.Cursor; +import java.lang.reflect.InvocationTargetException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.swing.ImageIcon; +import javax.swing.SwingUtilities; +import javax.swing.SwingWorker; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.mirth.connect.client.core.Client; +import com.mirth.connect.client.core.ClientException; +import com.mirth.connect.client.core.UnauthorizedException; +import com.mirth.connect.client.core.api.servlets.UserServletInterface; +import com.mirth.connect.client.ui.util.DisplayUtil; +import com.mirth.connect.model.ExtendedLoginStatus; +import com.mirth.connect.model.LoginStatus; +import com.mirth.connect.plugins.MultiFactorAuthenticationClientPlugin; + +class DefaultLoginPanel extends javax.swing.JFrame implements LoginPanel { + + private static final Logger logger = LogManager.getLogger(DefaultLoginPanel.class); + private static final String ERROR_MESSAGE = "There was an error connecting to the server at the specified address. Please verify that the server is up and running."; + + private LoginSuccessHandler onSuccess; + + DefaultLoginPanel() { + initComponents(); + DisplayUtil.setResizable(this, false); + jLabel2.setForeground(UIConstants.HEADER_TITLE_TEXT_COLOR); + jLabel5.setForeground(UIConstants.HEADER_TITLE_TEXT_COLOR); + setDefaultCloseOperation(EXIT_ON_CLOSE); + setIconImage(BrandingConstants.FAVICON.getImage()); + ImageIcon imageIcon = BrandingConstants.LOGO; // load the image to a imageIcon + mirthCorpImage.setIcon(imageIcon); + mirthCorpImage.setText(""); + mirthCorpImage.setToolTipText(BrandingConstants.COMPANY_TOOLTIP); + mirthCorpImage.setCursor(new Cursor(Cursor.HAND_CURSOR)); + + mirthCorpImage.addMouseListener(new java.awt.event.MouseAdapter() { + + public void mouseClicked(java.awt.event.MouseEvent evt) { + BareBonesBrowserLaunch.openURL(BrandingConstants.COMPANY_URL); + } + }); + + mirthCorpImage1.setIcon(imageIcon); + mirthCorpImage1.setText(""); + mirthCorpImage1.setToolTipText(BrandingConstants.COMPANY_TOOLTIP); + mirthCorpImage1.setCursor(new Cursor(Cursor.HAND_CURSOR)); + + mirthCorpImage1.addMouseListener(new java.awt.event.MouseAdapter() { + + public void mouseClicked(java.awt.event.MouseEvent evt) { + BareBonesBrowserLaunch.openURL(BrandingConstants.COMPANY_URL); + } + }); + + placeholderButton.setVisible(false); + + errorTextArea.setBackground(Color.WHITE); + errorTextArea.setDisabledTextColor(Color.RED); + } + + @Override + public void initialize(String mirthServer, String version, String user, String pass, LoginSuccessHandler onSuccess) { + if (SwingUtilities.isEventDispatchThread()) { + doInitialize(mirthServer, version, user, pass, onSuccess); + } else { + SwingUtilities.invokeLater(() -> doInitialize(mirthServer, version, user, pass, onSuccess)); + } + } + + private void doInitialize(String mirthServer, String version, String user, String pass, LoginSuccessHandler onSuccess) { + synchronized (this) { + // Do not initialize another login window if one is already visible + if (isVisible()) { + return; + } + this.onSuccess = onSuccess; + + setTitle(String.format("%s %s - Login", BrandingConstants.PRODUCT_NAME, version)); + setIconImage(BrandingConstants.FAVICON.getImage()); + + serverName.setText(mirthServer); + + // Make sure the login window is centered and not minimized + setLocationRelativeTo(null); + setState(Frame.NORMAL); + + errorPane.setVisible(false); + loggingIn.setVisible(false); + loginMain.setVisible(true); + loginProgress.setIndeterminate(false); + + setStatus("Logging in..."); + + username.setText(user); + password.setText(pass); + + username.grabFocus(); + + setVisible(true); + } + + if (user.length() > 0 && pass.length() > 0) { + loginButtonActionPerformed(null); + } + } + + // @formatter:off + /** + * 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. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + + loginMain = new javax.swing.JPanel(); + closeButton = new javax.swing.JButton(); + loginButton = new javax.swing.JButton(); + jSeparator1 = new javax.swing.JSeparator(); + serverName = new javax.swing.JTextField(); + jLabel1 = new javax.swing.JLabel(); + jLabel3 = new javax.swing.JLabel(); + mirthHeadingPanel2 = new com.mirth.connect.client.ui.MirthHeadingPanel(); + jLabel2 = new javax.swing.JLabel(); + username = new javax.swing.JTextField(); + jLabel6 = new javax.swing.JLabel(); + password = new javax.swing.JPasswordField(); + mirthCorpImage = new javax.swing.JLabel(); + errorPane = new javax.swing.JScrollPane(); + errorTextArea = new javax.swing.JTextArea(); + loggingIn = new javax.swing.JPanel(); + mirthHeadingPanel1 = new com.mirth.connect.client.ui.MirthHeadingPanel(); + jLabel5 = new javax.swing.JLabel(); + loginProgress = new javax.swing.JProgressBar(); + status = new javax.swing.JLabel(); + jSeparator2 = new javax.swing.JSeparator(); + mirthCorpImage1 = new javax.swing.JLabel(); + placeholderButton = new javax.swing.JButton(); + + setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); + setTitle(String.format("%s - Login", BrandingConstants.PRODUCT_NAME)); + setIconImage(BrandingConstants.FAVICON.getImage()); + + loginMain.setBackground(new java.awt.Color(255, 255, 255)); + loginMain.setName(""); // NOI18N + + closeButton.setText("Exit"); + closeButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + closeButtonActionPerformed(evt); + } + }); + + loginButton.setText("Login"); + loginButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + loginButtonActionPerformed(evt); + } + }); + + serverName.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + serverNameActionPerformed(evt); + } + }); + + jLabel1.setText("Server:"); + + jLabel3.setText("Username:"); + + jLabel2.setFont(new java.awt.Font("Tahoma", 1, 18)); // NOI18N + jLabel2.setForeground(new java.awt.Color(255, 255, 255)); + jLabel2.setText(String.format("%s Login", BrandingConstants.PRODUCT_NAME)); + + javax.swing.GroupLayout mirthHeadingPanel2Layout = new javax.swing.GroupLayout(mirthHeadingPanel2); + mirthHeadingPanel2.setLayout(mirthHeadingPanel2Layout); + mirthHeadingPanel2Layout.setHorizontalGroup( + mirthHeadingPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(mirthHeadingPanel2Layout.createSequentialGroup() + .addContainerGap() + .addComponent(jLabel2, javax.swing.GroupLayout.DEFAULT_SIZE, 358, Short.MAX_VALUE) + .addContainerGap()) + ); + mirthHeadingPanel2Layout.setVerticalGroup( + mirthHeadingPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(mirthHeadingPanel2Layout.createSequentialGroup() + .addContainerGap() + .addComponent(jLabel2, javax.swing.GroupLayout.DEFAULT_SIZE, 27, Short.MAX_VALUE) + .addContainerGap()) + ); + + username.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + usernameActionPerformed(evt); + } + }); + + jLabel6.setText("Password:"); + + password.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + passwordActionPerformed(evt); + } + }); + + mirthCorpImage.setText(" "); + + errorPane.setBorder(null); + errorPane.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + errorPane.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER); + + errorTextArea.setColumns(20); + errorTextArea.setEditable(false); + errorTextArea.setFont(new java.awt.Font("Tahoma", 0, 11)); + errorTextArea.setLineWrap(true); + errorTextArea.setText("There was an error connecting to the server at the specified address. Please verify that the server is up and running."); + errorTextArea.setWrapStyleWord(true); + errorTextArea.setEnabled(false); + errorPane.setViewportView(errorTextArea); + + javax.swing.GroupLayout loginMainLayout = new javax.swing.GroupLayout(loginMain); + loginMain.setLayout(loginMainLayout); + loginMainLayout.setHorizontalGroup( + loginMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(loginMainLayout.createSequentialGroup() + .addContainerGap() + .addComponent(jSeparator1, javax.swing.GroupLayout.DEFAULT_SIZE, 358, Short.MAX_VALUE) + .addContainerGap()) + .addComponent(mirthHeadingPanel2, javax.swing.GroupLayout.DEFAULT_SIZE, 378, Short.MAX_VALUE) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, loginMainLayout.createSequentialGroup() + .addContainerGap() + .addComponent(mirthCorpImage) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 235, Short.MAX_VALUE) + .addComponent(loginButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(closeButton) + .addContainerGap()) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, loginMainLayout.createSequentialGroup() + .addGap(51, 51, 51) + .addGroup(loginMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jLabel6, javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(jLabel3, javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(jLabel1, javax.swing.GroupLayout.Alignment.TRAILING)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(loginMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) + .addComponent(serverName, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, 220, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(password, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, 220, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(username, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, 220, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGap(51, 51, 51)) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, loginMainLayout.createSequentialGroup() + .addContainerGap(57, Short.MAX_VALUE) + .addComponent(errorPane, javax.swing.GroupLayout.PREFERRED_SIZE, 264, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(57, 57, 57)) + ); + + loginMainLayout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {closeButton, loginButton}); + + loginMainLayout.setVerticalGroup( + loginMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, loginMainLayout.createSequentialGroup() + .addComponent(mirthHeadingPanel2, javax.swing.GroupLayout.PREFERRED_SIZE, 49, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(22, 22, 22) + .addGroup(loginMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jLabel1) + .addComponent(serverName, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(loginMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jLabel3) + .addComponent(username, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(loginMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(password, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel6)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(errorPane, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 15, Short.MAX_VALUE) + .addComponent(jSeparator1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(loginMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(closeButton) + .addComponent(loginButton) + .addComponent(mirthCorpImage)) + .addContainerGap()) + ); + + loggingIn.setBackground(new java.awt.Color(255, 255, 255)); + + jLabel5.setFont(new java.awt.Font("Tahoma", 1, 18)); // NOI18N + jLabel5.setForeground(new java.awt.Color(255, 255, 255)); + jLabel5.setText(String.format("%s Login", BrandingConstants.PRODUCT_NAME)); + + javax.swing.GroupLayout mirthHeadingPanel1Layout = new javax.swing.GroupLayout(mirthHeadingPanel1); + mirthHeadingPanel1.setLayout(mirthHeadingPanel1Layout); + mirthHeadingPanel1Layout.setHorizontalGroup( + mirthHeadingPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(mirthHeadingPanel1Layout.createSequentialGroup() + .addContainerGap() + .addComponent(jLabel5, javax.swing.GroupLayout.PREFERRED_SIZE, 326, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap(42, Short.MAX_VALUE)) + ); + mirthHeadingPanel1Layout.setVerticalGroup( + mirthHeadingPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(mirthHeadingPanel1Layout.createSequentialGroup() + .addContainerGap() + .addComponent(jLabel5, javax.swing.GroupLayout.DEFAULT_SIZE, 27, Short.MAX_VALUE) + .addContainerGap()) + ); + + loginProgress.setDoubleBuffered(true); + + status.setText("Please wait: Logging in..."); + + mirthCorpImage1.setText(" "); + + placeholderButton.setText("Placeholder"); + + javax.swing.GroupLayout loggingInLayout = new javax.swing.GroupLayout(loggingIn); + loggingIn.setLayout(loggingInLayout); + loggingInLayout.setHorizontalGroup( + loggingInLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(mirthHeadingPanel1, javax.swing.GroupLayout.DEFAULT_SIZE, 378, Short.MAX_VALUE) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, loggingInLayout.createSequentialGroup() + .addContainerGap() + .addComponent(loginProgress, javax.swing.GroupLayout.DEFAULT_SIZE, 358, Short.MAX_VALUE) + .addContainerGap()) + .addGroup(loggingInLayout.createSequentialGroup() + .addContainerGap() + .addComponent(status) + .addContainerGap(247, Short.MAX_VALUE)) + .addGroup(loggingInLayout.createSequentialGroup() + .addContainerGap() + .addComponent(mirthCorpImage1) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 268, Short.MAX_VALUE) + .addComponent(placeholderButton) + .addContainerGap()) + .addGroup(loggingInLayout.createSequentialGroup() + .addContainerGap() + .addComponent(jSeparator2, javax.swing.GroupLayout.DEFAULT_SIZE, 358, Short.MAX_VALUE) + .addContainerGap()) + ); + loggingInLayout.setVerticalGroup( + loggingInLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(loggingInLayout.createSequentialGroup() + .addComponent(mirthHeadingPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, 49, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(45, 45, 45) + .addComponent(status) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(loginProgress, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 82, Short.MAX_VALUE) + .addComponent(jSeparator2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(loggingInLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(mirthCorpImage1) + .addComponent(placeholderButton)) + .addContainerGap()) + ); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(loginMain, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(loggingIn, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(loginMain, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(loggingIn, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + ); + + pack(); + }// //GEN-END:initComponents + // @formatter:on + + private void usernameActionPerformed(java.awt.event.ActionEvent evt)// GEN-FIRST:event_usernameActionPerformed + {// GEN-HEADEREND:event_usernameActionPerformed + loginButtonActionPerformed(null); + }// GEN-LAST:event_usernameActionPerformed + + private void serverNameActionPerformed(java.awt.event.ActionEvent evt)// GEN-FIRST:event_serverNameActionPerformed + {// GEN-HEADEREND:event_serverNameActionPerformed + loginButtonActionPerformed(null); + }// GEN-LAST:event_serverNameActionPerformed + + private void passwordActionPerformed(java.awt.event.ActionEvent evt)// GEN-FIRST:event_passwordActionPerformed + {// GEN-HEADEREND:event_passwordActionPerformed + loginButtonActionPerformed(null); + }// GEN-LAST:event_passwordActionPerformed + + private void loginButtonActionPerformed(java.awt.event.ActionEvent evt)// GEN-FIRST:event_loginButtonActionPerformed + {// GEN-HEADEREND:event_loginButtonActionPerformed + errorPane.setVisible(false); + DefaultLoginPanel loginPanel = this; + + SwingWorker worker = new SwingWorker() { + + public Void doInBackground() { + boolean errorOccurred = false; + + try { + String server = serverName.getText(); + Client client = new Client(server, PlatformUI.HTTPS_PROTOCOLS, PlatformUI.HTTPS_CIPHER_SUITES); + PlatformUI.SERVER_URL = server; + + // Attempt to login + LoginStatus loginStatus = null; + try { + Map> customHeaders = new HashMap>(); + customHeaders.put(UserServletInterface.LOGIN_SERVER_URL_HEADER, Collections.singletonList(PlatformUI.SERVER_URL)); + loginStatus = client.getServlet(UserServletInterface.class, null, customHeaders).login(username.getText(), String.valueOf(password.getPassword())); + } catch (ClientException ex) { + ex.printStackTrace(); + + if (ex instanceof UnauthorizedException) { + UnauthorizedException e2 = (UnauthorizedException) ex; + if (e2.getResponse() != null && e2.getResponse() instanceof LoginStatus) { + loginStatus = (LoginStatus) e2.getResponse(); + } + } + + // Leave loginStatus null, the error message will be set to the default + } + + // If SUCCESS or SUCCESS_GRACE_PERIOD + if (loginStatus != null && loginStatus.isSuccess()) { + if (!onSuccess.handle(client, loginStatus, username.getText())) { + loginPanel.setVisible(false); + loginPanel.initialize(PlatformUI.SERVER_URL, PlatformUI.CLIENT_VERSION, "", "", onSuccess); + } + } else { + // Assume failure unless overridden by a plugin + errorOccurred = true; + + if (loginStatus instanceof ExtendedLoginStatus) { + ExtendedLoginStatus extendedLoginStatus = (ExtendedLoginStatus) loginStatus; + + if (StringUtils.isNotBlank(extendedLoginStatus.getClientPluginClass())) { + String updatedUsername = StringUtils.defaultString(loginStatus.getUpdatedUsername(), username.getText()); + MultiFactorAuthenticationClientPlugin plugin = (MultiFactorAuthenticationClientPlugin) Class.forName(extendedLoginStatus.getClientPluginClass()).newInstance(); + + loginStatus = plugin.authenticate(DefaultLoginPanel.this, client, updatedUsername, loginStatus); + + if (loginStatus != null && loginStatus.isSuccess()) { + errorOccurred = false; + if (!onSuccess.handle(client, loginStatus, username.getText())) { + loginPanel.setVisible(false); + loginPanel.initialize(PlatformUI.SERVER_URL, PlatformUI.CLIENT_VERSION, "", "", onSuccess); + } + } + } + } + } + + if (errorOccurred) { + if (loginStatus != null) { + setError(loginStatus.getMessage()); + } else { + setError(ERROR_MESSAGE); + } + } + } catch (Throwable t) { + setError(ERROR_MESSAGE); + t.printStackTrace(); + } + + return null; + } + + public void done() {} + }; + worker.execute(); + + loggingIn.setVisible(true); + loginMain.setVisible(false); + loginProgress.setIndeterminate(true); + }// GEN-LAST:event_loginButtonActionPerformed + + /** + * If the button is "Next" instead of "Finish" then it moves on to the next options. Otherwise, + * it creates the new channel. + */ + private void closeButtonActionPerformed(java.awt.event.ActionEvent evt)// GEN-FIRST:event_closeButtonActionPerformed + {// GEN-HEADEREND:event_closeButtonActionPerformed + this.dispose(); + System.exit(0); + }// GEN-LAST:event_closeButtonActionPerformed + + @Override + public void setVisible(boolean visible) { + if (SwingUtilities.isEventDispatchThread()) { + super.setVisible(visible); + } else { + SwingUtilities.invokeLater(() -> super.setVisible(visible)); + } + } + + @Override + public void setStatus(String status) { + SwingUtilities.invokeLater(() -> this.status.setText("Please wait: " + status)); + } + + private void setError(String status) { + SwingUtilities.invokeLater(() -> { + errorTextArea.setText(status); + errorPane.setVisible(true); + loggingIn.setVisible(false); + loginMain.setVisible(true); + loginProgress.setIndeterminate(false); + password.grabFocus(); + }); + } + + @Override + public boolean showLoginNotification(String title, String message) { + // Called from a background thread (SwingWorker.doInBackground via handleLoginSuccess). + // Swing dialogs must be created and shown on the EDT to avoid deadlocks. + final AtomicBoolean accepted = new AtomicBoolean(false); + try { + SwingUtilities.invokeAndWait(() -> { + CustomBannerPanelDialog dialog = new CustomBannerPanelDialog(this, title, message); + accepted.set(dialog.isAccepted()); + }); + } catch (InterruptedException e) { + logger.warn("Login notification dialog interrupted; treating as declined"); + Thread.currentThread().interrupt(); + } catch (InvocationTargetException e) { + throw new RuntimeException(e.getCause()); + } + return accepted.get(); + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton closeButton; + private javax.swing.JScrollPane errorPane; + private javax.swing.JTextArea errorTextArea; + private javax.swing.JLabel jLabel1; + private javax.swing.JLabel jLabel2; + private javax.swing.JLabel jLabel3; + private javax.swing.JLabel jLabel5; + private javax.swing.JLabel jLabel6; + private javax.swing.JSeparator jSeparator1; + private javax.swing.JSeparator jSeparator2; + private javax.swing.JPanel loggingIn; + private javax.swing.JButton loginButton; + private javax.swing.JPanel loginMain; + private javax.swing.JProgressBar loginProgress; + private javax.swing.JLabel mirthCorpImage; + private javax.swing.JLabel mirthCorpImage1; + private com.mirth.connect.client.ui.MirthHeadingPanel mirthHeadingPanel1; + private com.mirth.connect.client.ui.MirthHeadingPanel mirthHeadingPanel2; + private javax.swing.JPasswordField password; + private javax.swing.JButton placeholderButton; + private javax.swing.JTextField serverName; + private javax.swing.JLabel status; + private javax.swing.JTextField username; + // End of variables declaration//GEN-END:variables +} diff --git a/client/src/com/mirth/connect/client/ui/Frame.java b/client/src/com/mirth/connect/client/ui/Frame.java index 633a2791aa..e5c85669c3 100644 --- a/client/src/com/mirth/connect/client/ui/Frame.java +++ b/client/src/com/mirth/connect/client/ui/Frame.java @@ -1,11 +1,6 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: Mirth Corporation +// SPDX-FileCopyrightText: 2025-2026 Open Integration Engine Contributors package com.mirth.connect.client.ui; @@ -57,6 +52,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.function.Consumer; import java.util.prefs.Preferences; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -541,15 +537,13 @@ public void eventDispatched(AWTEvent e) /** * Called to set up this main window frame. */ - public void setupFrame(Client mirthClient) throws ClientException { - - LoginPanel login = LoginPanel.getInstance(); + public void setupFrame(Client mirthClient, Consumer statusCallback) throws ClientException { // Initialize the send message dialog editMessageDialog = new EditMessageDialog(); this.mirthClient = mirthClient; - login.setStatus("Loading extensions..."); + statusCallback.accept("Loading extensions..."); try { loadExtensionMetaData(); } catch (ClientException e) { @@ -589,10 +583,10 @@ public void setupFrame(Client mirthClient) throws ClientException { } setInitialVisibleTasks(); - login.setStatus("Loading preferences..."); + statusCallback.accept("Loading preferences..."); userPreferences = Preferences.userNodeForPackage(Mirth.class); userPreferences.put("defaultServer", PlatformUI.SERVER_URL); - login.setStatus("Loading GUI components..."); + statusCallback.accept("Loading GUI components..."); splitPane.setDividerSize(0); splitPane.setBorder(BorderFactory.createEmptyBorder()); @@ -661,15 +655,15 @@ public void setupFrame(Client mirthClient) throws ClientException { } setCurrentTaskPaneContainer(taskPaneContainer); - login.setStatus("Loading dashboard..."); + statusCallback.accept("Loading dashboard..."); doShowDashboard(); - login.setStatus("Loading channel editor..."); + statusCallback.accept("Loading channel editor..."); channelEditPanel = new ChannelSetup(); - login.setStatus("Loading alert editor..."); + statusCallback.accept("Loading alert editor..."); if (alertEditPanel == null) { alertEditPanel = new DefaultAlertEditPanel(); } - login.setStatus("Loading message browser..."); + statusCallback.accept("Loading message browser..."); messageBrowser = new MessageBrowser(); // Refresh code templates after extensions have been loaded @@ -1524,7 +1518,7 @@ public void alertThrowable(Component parentComponent, Throwable t, String custom } mirthClient.close(); this.dispose(); - LoginPanel.getInstance().initialize(PlatformUI.SERVER_URL, PlatformUI.CLIENT_VERSION, "", ""); + Mirth.showLogin(); return; } else if (t.getCause() != null && t.getCause() instanceof HttpHostConnectException && (StringUtils.contains(t.getCause().getMessage(), "Connection refused") || StringUtils.contains(t.getCause().getMessage(), "Host is down"))) { connectionError = true; @@ -1542,7 +1536,7 @@ public void alertThrowable(Component parentComponent, Throwable t, String custom } mirthClient.close(); this.dispose(); - LoginPanel.getInstance().initialize(PlatformUI.SERVER_URL, PlatformUI.CLIENT_VERSION, "", ""); + Mirth.showLogin(); return; } } @@ -2292,7 +2286,7 @@ public boolean logout(boolean quit, boolean confirmFirst) { this.dispose(); if (!quit) { - LoginPanel.getInstance().initialize(PlatformUI.SERVER_URL, PlatformUI.CLIENT_VERSION, "", ""); + Mirth.showLogin(); } return true; diff --git a/client/src/com/mirth/connect/client/ui/LoginPanel.java b/client/src/com/mirth/connect/client/ui/LoginPanel.java index 0414b7accd..de439c8412 100644 --- a/client/src/com/mirth/connect/client/ui/LoginPanel.java +++ b/client/src/com/mirth/connect/client/ui/LoginPanel.java @@ -1,685 +1,149 @@ // SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: Mirth Corporation -// SPDX-FileCopyrightText: 2025 Mitch Gaffigan and Tony Germano +// SPDX-FileCopyrightText: 2025 Mitch Gaffigan +// SPDX-FileCopyrightText: 2026 Tony Germano package com.mirth.connect.client.ui; -import static com.mirth.connect.client.core.BrandingConstants.CHECK_FOR_NOTIFICATIONS; - -import java.awt.Color; -import java.awt.Cursor; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.prefs.Preferences; - -import javax.swing.ImageIcon; -import javax.swing.SwingWorker; - -import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; - import com.mirth.connect.client.core.Client; import com.mirth.connect.client.core.ClientException; -import com.mirth.connect.client.core.ConnectServiceUtil; -import com.mirth.connect.client.core.UnauthorizedException; -import com.mirth.connect.client.core.api.servlets.UserServletInterface; -import com.mirth.connect.client.ui.util.DisplayUtil; -import com.mirth.connect.model.ExtendedLoginStatus; import com.mirth.connect.model.LoginStatus; -import com.mirth.connect.model.PublicServerSettings; -import com.mirth.connect.model.User; -import com.mirth.connect.model.converters.ObjectXMLSerializer; -import com.mirth.connect.plugins.MultiFactorAuthenticationClientPlugin; -import com.mirth.connect.util.MirthSSLUtil; - -public class LoginPanel extends javax.swing.JFrame { - - private Client client; - private static final String ERROR_MESSAGE = "There was an error connecting to the server at the specified address. Please verify that the server is up and running."; - private static LoginPanel instance = null; - - private LoginPanel() { - initComponents(); - DisplayUtil.setResizable(this, false); - jLabel2.setForeground(UIConstants.HEADER_TITLE_TEXT_COLOR); - jLabel5.setForeground(UIConstants.HEADER_TITLE_TEXT_COLOR); - setDefaultCloseOperation(EXIT_ON_CLOSE); - setIconImage(BrandingConstants.FAVICON.getImage()); - ImageIcon imageIcon = BrandingConstants.LOGO; // load the image to a imageIcon - mirthCorpImage.setIcon(imageIcon); - mirthCorpImage.setText(""); - mirthCorpImage.setToolTipText(BrandingConstants.COMPANY_TOOLTIP); - mirthCorpImage.setCursor(new Cursor(Cursor.HAND_CURSOR)); - - mirthCorpImage.addMouseListener(new java.awt.event.MouseAdapter() { - - public void mouseClicked(java.awt.event.MouseEvent evt) { - BareBonesBrowserLaunch.openURL(BrandingConstants.COMPANY_URL); - } - }); - - mirthCorpImage1.setIcon(imageIcon); - mirthCorpImage1.setText(""); - mirthCorpImage1.setToolTipText(BrandingConstants.COMPANY_TOOLTIP); - mirthCorpImage1.setCursor(new Cursor(Cursor.HAND_CURSOR)); - - mirthCorpImage1.addMouseListener(new java.awt.event.MouseAdapter() { - - public void mouseClicked(java.awt.event.MouseEvent evt) { - BareBonesBrowserLaunch.openURL(BrandingConstants.COMPANY_URL); - } - }); - - placeholderButton.setVisible(false); - - errorTextArea.setBackground(Color.WHITE); - errorTextArea.setDisabledTextColor(Color.RED); - } - - public static LoginPanel getInstance() { - synchronized (LoginPanel.class) { - if (instance == null) { - instance = new LoginPanel(); - } - return instance; - } - } - - public void initialize(String mirthServer, String version, String user, String pass) { - synchronized (this) { - // Do not initialize another login window if one is already visible - if (isVisible()) { - return; - } - - PlatformUI.CLIENT_VERSION = version; - - setTitle(String.format("%s %s - Login", BrandingConstants.PRODUCT_NAME, version)); - setIconImage(BrandingConstants.FAVICON.getImage()); - - serverName.setText(mirthServer); - - // Make sure the login window is centered and not minimized - setLocationRelativeTo(null); - setState(Frame.NORMAL); - - errorPane.setVisible(false); - loggingIn.setVisible(false); - loginMain.setVisible(true); - loginProgress.setIndeterminate(false); - - setStatus("Logging in..."); - username.setText(user); - password.setText(pass); +/** + * Abstraction for the login panel so that alternative UI toolkit implementations + * (e.g. JavaFX) can replace the default Swing-based panel. + * + *

Discovery

+ * Implementations are discovered at runtime via {@link java.util.ServiceLoader}. + * To provide a custom login panel, add a + * {@code META-INF/services/com.mirth.connect.client.ui.LoginPanel} file whose + * single line is the fully-qualified name of the implementing class. + * The custom class must have a public no-arg constructor (required by + * {@link java.util.ServiceLoader}). + * If multiple custom implementations are found, the first one discovered + * is used (ordering depends on the classloader). + * If no custom implementation is found on the classpath, + * {@link DefaultLoginPanel} (the built-in Swing implementation) is used as + * a hardcoded fallback. {@code DefaultLoginPanel} itself is not + * registered as an SPI provider and uses a package-private constructor + * (it is instantiated directly by {@link LoginPanelFactory}). + * + *

Lifecycle

+ *
    + *
  1. The panel is instantiated once by {@link LoginPanelFactory}.
  2. + *
  3. {@link #initialize} is called each time a login screen is needed + * (initial startup and after logout / connection errors). + * If {@code user} and {@code pass} are both non-empty the implementation + * should attempt an automatic login.
  4. + *
  5. During the boot sequence after a successful login, + * {@link #setStatus} is called repeatedly to report loading progress.
  6. + *
  7. Once the main application frame is ready, {@link #setVisible(boolean) setVisible(false)} + * is called to hide the panel.
  8. + *
+ * + *

Multi-Factor Authentication

+ * Existing {@link com.mirth.connect.plugins.MultiFactorAuthenticationClientPlugin} + * implementations expect a {@link java.awt.Window} to be passed as their parent + * for dialog display. A Swing-based login panel (such as {@link DefaultLoginPanel}) + * can pass itself directly. Non-Swing implementations are free to define their + * own MFA workflow instead of using the plugin API. + * + *

Threading

+ * All methods on this interface ({@link #initialize}, {@link #setStatus}, + * {@link #setVisible}, {@link #isVisible}, and {@link #showLoginNotification}) + * may be called from any thread. Implementations must handle any necessary + * thread marshalling internally (e.g. posting to the UI thread). + *

+ * The thread from which the {@link LoginSuccessHandler} is invoked is + * determined by the implementation. {@link DefaultLoginPanel} calls the + * handler from a background thread. + * The handler (and anything it calls) must not assume it is on a UI thread. + * + * @see LoginPanelFactory + * @see DefaultLoginPanel + */ +public interface LoginPanel { - username.grabFocus(); - - setVisible(true); - } - - if (user.length() > 0 && pass.length() > 0) { - loginButtonActionPerformed(null); - } + /** + * Callback invoked by the login panel after the server confirms a + * successful authentication. + * + * @see #initialize + */ + @FunctionalInterface + interface LoginSuccessHandler { + /** + * Handle a successful login. + * + * @param client the authenticated {@link Client} connection + * @param loginStatus the status returned by the server + * @param userName the username that was used to log in + * @return {@code true} if the application booted successfully; + * {@code false} if the login flow should restart + * (e.g. the user cancelled a first-login dialog) + * @throws ClientException if a server communication error occurs + */ + boolean handle(Client client, LoginStatus loginStatus, String userName) throws ClientException; } - // @formatter:off /** - * 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. + * Initialize (or re-initialize) the login UI and make it visible. + *

+ * If both {@code user} and {@code pass} are non-empty, the implementation + * should start the login attempt automatically without waiting for the + * user to click a button. + *

+ * If the panel is already visible, the call should be ignored to + * prevent duplicate windows. + * + * @param mirthServer the server URL to display / connect to + * @param version the client version string, used in the window title + * @param user pre-filled username (may be empty) + * @param pass pre-filled password (may be empty) + * @param onSuccess callback to invoke after a successful authentication */ - // //GEN-BEGIN:initComponents - private void initComponents() { - - loginMain = new javax.swing.JPanel(); - closeButton = new javax.swing.JButton(); - loginButton = new javax.swing.JButton(); - jSeparator1 = new javax.swing.JSeparator(); - serverName = new javax.swing.JTextField(); - jLabel1 = new javax.swing.JLabel(); - jLabel3 = new javax.swing.JLabel(); - mirthHeadingPanel2 = new com.mirth.connect.client.ui.MirthHeadingPanel(); - jLabel2 = new javax.swing.JLabel(); - username = new javax.swing.JTextField(); - jLabel6 = new javax.swing.JLabel(); - password = new javax.swing.JPasswordField(); - mirthCorpImage = new javax.swing.JLabel(); - errorPane = new javax.swing.JScrollPane(); - errorTextArea = new javax.swing.JTextArea(); - loggingIn = new javax.swing.JPanel(); - mirthHeadingPanel1 = new com.mirth.connect.client.ui.MirthHeadingPanel(); - jLabel5 = new javax.swing.JLabel(); - loginProgress = new javax.swing.JProgressBar(); - status = new javax.swing.JLabel(); - jSeparator2 = new javax.swing.JSeparator(); - mirthCorpImage1 = new javax.swing.JLabel(); - placeholderButton = new javax.swing.JButton(); - - setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); - setTitle(String.format("%s - Login", BrandingConstants.PRODUCT_NAME)); - setIconImage(BrandingConstants.FAVICON.getImage()); - - loginMain.setBackground(new java.awt.Color(255, 255, 255)); - loginMain.setName(""); // NOI18N - - closeButton.setText("Exit"); - closeButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - closeButtonActionPerformed(evt); - } - }); - - loginButton.setText("Login"); - loginButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - loginButtonActionPerformed(evt); - } - }); - - serverName.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - serverNameActionPerformed(evt); - } - }); - - jLabel1.setText("Server:"); - - jLabel3.setText("Username:"); - - jLabel2.setFont(new java.awt.Font("Tahoma", 1, 18)); // NOI18N - jLabel2.setForeground(new java.awt.Color(255, 255, 255)); - jLabel2.setText(String.format("%s Login", BrandingConstants.PRODUCT_NAME)); - - javax.swing.GroupLayout mirthHeadingPanel2Layout = new javax.swing.GroupLayout(mirthHeadingPanel2); - mirthHeadingPanel2.setLayout(mirthHeadingPanel2Layout); - mirthHeadingPanel2Layout.setHorizontalGroup( - mirthHeadingPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(mirthHeadingPanel2Layout.createSequentialGroup() - .addContainerGap() - .addComponent(jLabel2, javax.swing.GroupLayout.DEFAULT_SIZE, 358, Short.MAX_VALUE) - .addContainerGap()) - ); - mirthHeadingPanel2Layout.setVerticalGroup( - mirthHeadingPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(mirthHeadingPanel2Layout.createSequentialGroup() - .addContainerGap() - .addComponent(jLabel2, javax.swing.GroupLayout.DEFAULT_SIZE, 27, Short.MAX_VALUE) - .addContainerGap()) - ); - - username.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - usernameActionPerformed(evt); - } - }); - - jLabel6.setText("Password:"); - - password.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - passwordActionPerformed(evt); - } - }); - - mirthCorpImage.setText(" "); - - errorPane.setBorder(null); - errorPane.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); - errorPane.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER); - - errorTextArea.setColumns(20); - errorTextArea.setEditable(false); - errorTextArea.setFont(new java.awt.Font("Tahoma", 0, 11)); - errorTextArea.setLineWrap(true); - errorTextArea.setText("There was an error connecting to the server at the specified address. Please verify that the server is up and running."); - errorTextArea.setWrapStyleWord(true); - errorTextArea.setEnabled(false); - errorPane.setViewportView(errorTextArea); - - javax.swing.GroupLayout loginMainLayout = new javax.swing.GroupLayout(loginMain); - loginMain.setLayout(loginMainLayout); - loginMainLayout.setHorizontalGroup( - loginMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(loginMainLayout.createSequentialGroup() - .addContainerGap() - .addComponent(jSeparator1, javax.swing.GroupLayout.DEFAULT_SIZE, 358, Short.MAX_VALUE) - .addContainerGap()) - .addComponent(mirthHeadingPanel2, javax.swing.GroupLayout.DEFAULT_SIZE, 378, Short.MAX_VALUE) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, loginMainLayout.createSequentialGroup() - .addContainerGap() - .addComponent(mirthCorpImage) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 235, Short.MAX_VALUE) - .addComponent(loginButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(closeButton) - .addContainerGap()) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, loginMainLayout.createSequentialGroup() - .addGap(51, 51, 51) - .addGroup(loginMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jLabel6, javax.swing.GroupLayout.Alignment.TRAILING) - .addComponent(jLabel3, javax.swing.GroupLayout.Alignment.TRAILING) - .addComponent(jLabel1, javax.swing.GroupLayout.Alignment.TRAILING)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(loginMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) - .addComponent(serverName, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, 220, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(password, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, 220, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(username, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, 220, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGap(51, 51, 51)) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, loginMainLayout.createSequentialGroup() - .addContainerGap(57, Short.MAX_VALUE) - .addComponent(errorPane, javax.swing.GroupLayout.PREFERRED_SIZE, 264, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(57, 57, 57)) - ); - - loginMainLayout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {closeButton, loginButton}); - - loginMainLayout.setVerticalGroup( - loginMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, loginMainLayout.createSequentialGroup() - .addComponent(mirthHeadingPanel2, javax.swing.GroupLayout.PREFERRED_SIZE, 49, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(22, 22, 22) - .addGroup(loginMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(jLabel1) - .addComponent(serverName, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(loginMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(jLabel3) - .addComponent(username, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(loginMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(password, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(jLabel6)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(errorPane, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 15, Short.MAX_VALUE) - .addComponent(jSeparator1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(loginMainLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(closeButton) - .addComponent(loginButton) - .addComponent(mirthCorpImage)) - .addContainerGap()) - ); + void initialize(String mirthServer, String version, String user, String pass, LoginSuccessHandler onSuccess); - loggingIn.setBackground(new java.awt.Color(255, 255, 255)); - - jLabel5.setFont(new java.awt.Font("Tahoma", 1, 18)); // NOI18N - jLabel5.setForeground(new java.awt.Color(255, 255, 255)); - jLabel5.setText(String.format("%s Login", BrandingConstants.PRODUCT_NAME)); - - javax.swing.GroupLayout mirthHeadingPanel1Layout = new javax.swing.GroupLayout(mirthHeadingPanel1); - mirthHeadingPanel1.setLayout(mirthHeadingPanel1Layout); - mirthHeadingPanel1Layout.setHorizontalGroup( - mirthHeadingPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(mirthHeadingPanel1Layout.createSequentialGroup() - .addContainerGap() - .addComponent(jLabel5, javax.swing.GroupLayout.PREFERRED_SIZE, 326, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap(42, Short.MAX_VALUE)) - ); - mirthHeadingPanel1Layout.setVerticalGroup( - mirthHeadingPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(mirthHeadingPanel1Layout.createSequentialGroup() - .addContainerGap() - .addComponent(jLabel5, javax.swing.GroupLayout.DEFAULT_SIZE, 27, Short.MAX_VALUE) - .addContainerGap()) - ); - - loginProgress.setDoubleBuffered(true); - - status.setText("Please wait: Logging in..."); - - mirthCorpImage1.setText(" "); - - placeholderButton.setText("Placeholder"); - - javax.swing.GroupLayout loggingInLayout = new javax.swing.GroupLayout(loggingIn); - loggingIn.setLayout(loggingInLayout); - loggingInLayout.setHorizontalGroup( - loggingInLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(mirthHeadingPanel1, javax.swing.GroupLayout.DEFAULT_SIZE, 378, Short.MAX_VALUE) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, loggingInLayout.createSequentialGroup() - .addContainerGap() - .addComponent(loginProgress, javax.swing.GroupLayout.DEFAULT_SIZE, 358, Short.MAX_VALUE) - .addContainerGap()) - .addGroup(loggingInLayout.createSequentialGroup() - .addContainerGap() - .addComponent(status) - .addContainerGap(247, Short.MAX_VALUE)) - .addGroup(loggingInLayout.createSequentialGroup() - .addContainerGap() - .addComponent(mirthCorpImage1) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 268, Short.MAX_VALUE) - .addComponent(placeholderButton) - .addContainerGap()) - .addGroup(loggingInLayout.createSequentialGroup() - .addContainerGap() - .addComponent(jSeparator2, javax.swing.GroupLayout.DEFAULT_SIZE, 358, Short.MAX_VALUE) - .addContainerGap()) - ); - loggingInLayout.setVerticalGroup( - loggingInLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(loggingInLayout.createSequentialGroup() - .addComponent(mirthHeadingPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, 49, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(45, 45, 45) - .addComponent(status) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(loginProgress, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 82, Short.MAX_VALUE) - .addComponent(jSeparator2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(loggingInLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(mirthCorpImage1) - .addComponent(placeholderButton)) - .addContainerGap()) - ); - - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); - getContentPane().setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(loginMain, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(loggingIn, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(loginMain, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(loggingIn, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - ); - - pack(); - }// //GEN-END:initComponents - // @formatter:on - - private void usernameActionPerformed(java.awt.event.ActionEvent evt)// GEN-FIRST:event_usernameActionPerformed - {// GEN-HEADEREND:event_usernameActionPerformed - loginButtonActionPerformed(null); - }// GEN-LAST:event_usernameActionPerformed - - private void serverNameActionPerformed(java.awt.event.ActionEvent evt)// GEN-FIRST:event_serverNameActionPerformed - {// GEN-HEADEREND:event_serverNameActionPerformed - loginButtonActionPerformed(null); - }// GEN-LAST:event_serverNameActionPerformed - - private void passwordActionPerformed(java.awt.event.ActionEvent evt)// GEN-FIRST:event_passwordActionPerformed - {// GEN-HEADEREND:event_passwordActionPerformed - loginButtonActionPerformed(null); - }// GEN-LAST:event_passwordActionPerformed - - private void loginButtonActionPerformed(java.awt.event.ActionEvent evt)// GEN-FIRST:event_loginButtonActionPerformed - {// GEN-HEADEREND:event_loginButtonActionPerformed - errorPane.setVisible(false); - - SwingWorker worker = new SwingWorker() { - - public Void doInBackground() { - boolean errorOccurred = false; - - try { - String server = serverName.getText(); - client = new Client(server, PlatformUI.HTTPS_PROTOCOLS, PlatformUI.HTTPS_CIPHER_SUITES); - PlatformUI.SERVER_URL = server; - - // Attempt to login - LoginStatus loginStatus = null; - try { - Map> customHeaders = new HashMap>(); - customHeaders.put(UserServletInterface.LOGIN_SERVER_URL_HEADER, Collections.singletonList(PlatformUI.SERVER_URL)); - loginStatus = client.getServlet(UserServletInterface.class, null, customHeaders).login(username.getText(), String.valueOf(password.getPassword())); - } catch (ClientException ex) { - ex.printStackTrace(); - - if (ex instanceof UnauthorizedException) { - UnauthorizedException e2 = (UnauthorizedException) ex; - if (e2.getResponse() != null && e2.getResponse() instanceof LoginStatus) { - loginStatus = (LoginStatus) e2.getResponse(); - } - } - - // Leave loginStatus null, the error message will be set to the default - } - - // If SUCCESS or SUCCESS_GRACE_PERIOD - if (loginStatus != null && loginStatus.isSuccess()) { - if (!handleSuccess(loginStatus)) { - LoginPanel.getInstance().setVisible(false); - LoginPanel.getInstance().initialize(PlatformUI.SERVER_URL, PlatformUI.CLIENT_VERSION, "", ""); - } - } else { - // Assume failure unless overridden by a plugin - errorOccurred = true; - - if (loginStatus instanceof ExtendedLoginStatus) { - ExtendedLoginStatus extendedLoginStatus = (ExtendedLoginStatus) loginStatus; - - if (StringUtils.isNotBlank(extendedLoginStatus.getClientPluginClass())) { - String updatedUsername = StringUtils.defaultString(loginStatus.getUpdatedUsername(), username.getText()); - MultiFactorAuthenticationClientPlugin plugin = (MultiFactorAuthenticationClientPlugin) Class.forName(extendedLoginStatus.getClientPluginClass()).newInstance(); - - loginStatus = plugin.authenticate(LoginPanel.this, client, updatedUsername, loginStatus); - - if (loginStatus != null && loginStatus.isSuccess()) { - errorOccurred = false; - if (!handleSuccess(loginStatus)) { - LoginPanel.getInstance().setVisible(false); - LoginPanel.getInstance().initialize(PlatformUI.SERVER_URL, PlatformUI.CLIENT_VERSION, "", ""); - } - } - } - } - } - - if (errorOccurred) { - if (loginStatus != null) { - errorTextArea.setText(loginStatus.getMessage()); - } else { - errorTextArea.setText(ERROR_MESSAGE); - } - } - } catch (Throwable t) { - errorOccurred = true; - errorTextArea.setText(ERROR_MESSAGE); - t.printStackTrace(); - } - - if (errorOccurred) { - errorPane.setVisible(true); - loggingIn.setVisible(false); - loginMain.setVisible(true); - loginProgress.setIndeterminate(false); - password.grabFocus(); - } - - return null; - } - - private boolean handleSuccess(LoginStatus loginStatus) throws ClientException { - try { - PublicServerSettings publicServerSettings = client.getPublicServerSettings(); - - if (publicServerSettings.getLoginNotificationEnabled() == true) { - CustomBannerPanelDialog customBannerPanelDialog = new CustomBannerPanelDialog(LoginPanel.getInstance(), "Login Notification", publicServerSettings.getLoginNotificationMessage()); - boolean isAccepted = customBannerPanelDialog.isAccepted(); - - if (isAccepted == true) { - client.setUserNotificationAcknowledged(client.getCurrentUser().getId()); - } - else { - return false; - } - } - - String environmentName = publicServerSettings.getEnvironmentName(); - if (!StringUtils.isBlank(environmentName)) { - PlatformUI.ENVIRONMENT_NAME = environmentName; - } - - String serverName = publicServerSettings.getServerName(); - if (!StringUtils.isBlank(serverName)) { - PlatformUI.SERVER_NAME = serverName; - } else { - PlatformUI.SERVER_NAME = null; - } - - Color defaultBackgroundColor = publicServerSettings.getDefaultAdministratorBackgroundColor(); - if (defaultBackgroundColor != null) { - PlatformUI.DEFAULT_BACKGROUND_COLOR = defaultBackgroundColor; - } - } catch (ClientException e) { - PlatformUI.SERVER_NAME = null; - } - - try { - String database = (String) client.getAbout().get("database"); - if (!StringUtils.isBlank(database)) { - PlatformUI.SERVER_DATABASE = database; - } else { - PlatformUI.SERVER_DATABASE = null; - } - } catch (ClientException e) { - PlatformUI.SERVER_DATABASE = null; - } - - try { - Map map = client.getProtocolsAndCipherSuites(); - PlatformUI.SERVER_HTTPS_SUPPORTED_PROTOCOLS = map.get(MirthSSLUtil.KEY_SUPPORTED_PROTOCOLS); - PlatformUI.SERVER_HTTPS_ENABLED_CLIENT_PROTOCOLS = map.get(MirthSSLUtil.KEY_ENABLED_CLIENT_PROTOCOLS); - PlatformUI.SERVER_HTTPS_ENABLED_SERVER_PROTOCOLS = map.get(MirthSSLUtil.KEY_ENABLED_SERVER_PROTOCOLS); - PlatformUI.SERVER_HTTPS_SUPPORTED_CIPHER_SUITES = map.get(MirthSSLUtil.KEY_SUPPORTED_CIPHER_SUITES); - PlatformUI.SERVER_HTTPS_ENABLED_CIPHER_SUITES = map.get(MirthSSLUtil.KEY_ENABLED_CIPHER_SUITES); - } catch (ClientException e) { - } - - PlatformUI.USER_NAME = StringUtils.defaultString(loginStatus.getUpdatedUsername(), username.getText()); - setStatus("Authenticated..."); - new Mirth(client); - LoginPanel.getInstance().setVisible(false); - - User currentUser = PlatformUI.MIRTH_FRAME.getCurrentUser(PlatformUI.MIRTH_FRAME); - Properties userPreferences = new Properties(); - Set preferenceNames = new HashSet(); - preferenceNames.add("firstlogin"); - preferenceNames.add("checkForNotifications"); - preferenceNames.add("showNotificationPopup"); - preferenceNames.add("archivedNotifications"); - try { - userPreferences = client.getUserPreferences(currentUser.getId(), preferenceNames); - - // Display registration dialog if it's the user's first time logging in - String firstlogin = userPreferences.getProperty("firstlogin"); - if (firstlogin == null || BooleanUtils.toBoolean(firstlogin)) { - if (Integer.valueOf(currentUser.getId()) == 1) { - // if current user is user 1: - // 1. check system preferences for user information - // 2. if system preferences exist, populate screen using currentUser - Preferences preferences = Preferences.userNodeForPackage(Mirth.class); - String systemUserInfo = preferences.get("userLoginInfo", null); - if (systemUserInfo != null) { - String info[] = systemUserInfo.split(",", 0); - currentUser.setUsername(info[0]); - currentUser.setFirstName(info[1]); - currentUser.setLastName(info[2]); - currentUser.setEmail(info[3]); - currentUser.setCountry(info[4]); - currentUser.setStateTerritory(info[5]); - currentUser.setPhoneNumber(info[6]); - currentUser.setOrganization(info[7]); - currentUser.setRole(info[8]); - currentUser.setIndustry(info[9]); - currentUser.setDescription(info[10]); - } - } - FirstLoginDialog firstLoginDialog = new FirstLoginDialog(currentUser); - // if leaving the first login dialog without saving - if (!firstLoginDialog.getResult()) { - return false; - } - } else if (loginStatus.getStatus() == LoginStatus.Status.SUCCESS_GRACE_PERIOD) { - new ChangePasswordDialog(currentUser, loginStatus.getMessage()); - } - - // Check for new notifications from update server if enabled - String checkForNotifications = userPreferences.getProperty("checkForNotifications"); - if (CHECK_FOR_NOTIFICATIONS - && (checkForNotifications == null || BooleanUtils.toBoolean(checkForNotifications))) { - Set archivedNotifications = new HashSet(); - String archivedNotificationString = userPreferences.getProperty("archivedNotifications"); - if (archivedNotificationString != null) { - archivedNotifications = ObjectXMLSerializer.getInstance().deserialize(archivedNotificationString, Set.class); - } - // Update the Other Tasks pane with the unarchived notification count - int unarchivedNotifications = ConnectServiceUtil.getNotificationCount(PlatformUI.SERVER_ID, PlatformUI.SERVER_VERSION, LoadedExtensions.getInstance().getExtensionVersions(), archivedNotifications, PlatformUI.HTTPS_PROTOCOLS, PlatformUI.HTTPS_CIPHER_SUITES); - PlatformUI.MIRTH_FRAME.updateNotificationTaskName(unarchivedNotifications); - - // Display notification dialog if enabled and if there are new notifications - String showNotificationPopup = userPreferences.getProperty("showNotificationPopup"); - if (showNotificationPopup == null || BooleanUtils.toBoolean(showNotificationPopup)) { - if (unarchivedNotifications > 0) { - new NotificationDialog(); - } - } - } - } catch (ClientException e) { - PlatformUI.MIRTH_FRAME.alertThrowable(PlatformUI.MIRTH_FRAME, e); - } - - PlatformUI.MIRTH_FRAME.sendUsageStatistics(); - - return true; - } - - public void done() {} - }; - worker.execute(); - - loggingIn.setVisible(true); - loginMain.setVisible(false); - loginProgress.setIndeterminate(true); - }// GEN-LAST:event_loginButtonActionPerformed + /** + * Update the status text shown on the login UI. + *

+ * Called repeatedly during the post-login boot sequence to report + * loading progress (e.g. "Loading extensions…"). + * Implementations must be safe to call from any thread. + * + * @param status the progress message to display + */ + void setStatus(String status); /** - * If the button is "Next" instead of "Finish" then it moves on to the next options. Otherwise, - * it creates the new channel. + * Show or hide the login panel. + *

+ * After the main application frame is ready, {@code setVisible(false)} + * is called to dismiss the login UI. Implementations must be safe to + * call from any thread. + * + * @param visible {@code true} to show, {@code false} to hide */ - private void closeButtonActionPerformed(java.awt.event.ActionEvent evt)// GEN-FIRST:event_closeButtonActionPerformed - {// GEN-HEADEREND:event_closeButtonActionPerformed - this.dispose(); - System.exit(0); - }// GEN-LAST:event_closeButtonActionPerformed + void setVisible(boolean visible); - public void setStatus(String status) { - this.status.setText("Please wait: " + status); - } + /** + * Returns whether the login panel is currently visible. + * + * @return {@code true} if the panel is showing + */ + boolean isVisible(); - // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JButton closeButton; - private javax.swing.JScrollPane errorPane; - private javax.swing.JTextArea errorTextArea; - private javax.swing.JLabel jLabel1; - private javax.swing.JLabel jLabel2; - private javax.swing.JLabel jLabel3; - private javax.swing.JLabel jLabel5; - private javax.swing.JLabel jLabel6; - private javax.swing.JSeparator jSeparator1; - private javax.swing.JSeparator jSeparator2; - private javax.swing.JPanel loggingIn; - private javax.swing.JButton loginButton; - private javax.swing.JPanel loginMain; - private javax.swing.JProgressBar loginProgress; - private javax.swing.JLabel mirthCorpImage; - private javax.swing.JLabel mirthCorpImage1; - private com.mirth.connect.client.ui.MirthHeadingPanel mirthHeadingPanel1; - private com.mirth.connect.client.ui.MirthHeadingPanel mirthHeadingPanel2; - private javax.swing.JPasswordField password; - private javax.swing.JButton placeholderButton; - private javax.swing.JTextField serverName; - private javax.swing.JLabel status; - private javax.swing.JTextField username; - // End of variables declaration//GEN-END:variables + /** + * Display a modal login-notification banner and wait for the user to + * accept or cancel. + *

+ * This is called after a successful authentication when the server has + * login notifications enabled. If the user does not accept, the + * login flow is aborted and restarted. + * + * @param title the dialog title + * @param message the notification body text + * @return {@code true} if the user accepted the notification, + * {@code false} if they cancelled + */ + boolean showLoginNotification(String title, String message); } diff --git a/client/src/com/mirth/connect/client/ui/LoginPanelFactory.java b/client/src/com/mirth/connect/client/ui/LoginPanelFactory.java new file mode 100644 index 0000000000..83bb5c3f56 --- /dev/null +++ b/client/src/com/mirth/connect/client/ui/LoginPanelFactory.java @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025-2026 Open Integration Engine Contributors + +package com.mirth.connect.client.ui; + +import java.util.ServiceLoader; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Factory for obtaining the application's {@link LoginPanel} implementation. + * Uses {@link ServiceLoader} to discover custom implementations, falling back + * to {@link DefaultLoginPanel} if none are found. + */ +public class LoginPanelFactory { + + private static final Logger logger = LogManager.getLogger(LoginPanelFactory.class); + private static LoginPanel loginPanel = null; + + /** + * Returns the singleton {@link LoginPanel} instance, discovering it via + * {@link ServiceLoader} on first call. If multiple custom implementations + * are found, the first one discovered is used (ordering is classloader-dependent). + * Falls back to {@link DefaultLoginPanel} if no custom implementation is + * registered. {@code DefaultLoginPanel} itself should not be registered + * as an SPI provider — it is the hardcoded fallback. + */ + public static synchronized LoginPanel getLoginPanel() { + if (loginPanel == null) { + loginPanel = ServiceLoader.load(LoginPanel.class) + .findFirst() + .orElseGet(DefaultLoginPanel::new); + logger.info("Using LoginPanel: {}", loginPanel.getClass().getName()); + } + return loginPanel; + } +} diff --git a/client/src/com/mirth/connect/client/ui/Mirth.java b/client/src/com/mirth/connect/client/ui/Mirth.java index eacb683313..b5dc9410ca 100644 --- a/client/src/com/mirth/connect/client/ui/Mirth.java +++ b/client/src/com/mirth/connect/client/ui/Mirth.java @@ -3,12 +3,18 @@ package com.mirth.connect.client.ui; +import static com.mirth.connect.client.core.BrandingConstants.CHECK_FOR_NOTIFICATIONS; + import java.awt.Color; import java.awt.Toolkit; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.io.IOException; import java.util.prefs.Preferences; +import java.util.Map; +import java.util.Set; +import java.util.Properties; +import java.util.HashSet; import javax.imageio.ImageIO; import javax.swing.ImageIcon; @@ -22,6 +28,7 @@ import javax.swing.text.JTextComponent; import javax.swing.text.JTextComponent.KeyBinding; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.apache.logging.log4j.Level; @@ -36,6 +43,12 @@ import com.jgoodies.looks.plastic.PlasticXPLookAndFeel; import com.mirth.connect.client.core.Client; import com.mirth.connect.client.core.ClientException; +import com.mirth.connect.client.core.ConnectServiceUtil; +import com.mirth.connect.model.LoginStatus; +import com.mirth.connect.model.PublicServerSettings; +import com.mirth.connect.model.User; +import com.mirth.connect.model.converters.ObjectXMLSerializer; +import com.mirth.connect.util.MirthSSLUtil; /** * The main mirth class. Sets up the login and then authenticates the login information and sets up @@ -56,8 +69,9 @@ public Mirth(Client mirthClient) throws ClientException { UIManager.put("Tree.closedIcon", UIConstants.CLOSED_ICON); userPreferences = Preferences.userNodeForPackage(Mirth.class); - LoginPanel.getInstance().setStatus("Loading components..."); - PlatformUI.MIRTH_FRAME.setupFrame(mirthClient); + LoginPanel loginPanel = LoginPanelFactory.getLoginPanel(); + loginPanel.setStatus("Loading components..."); + PlatformUI.MIRTH_FRAME.setupFrame(mirthClient, loginPanel::setStatus); boolean maximized; int width; @@ -116,7 +130,7 @@ public static void aboutMac() { * @return quit */ public static boolean quitMac() { - return (LoginPanel.getInstance().isVisible() || (PlatformUI.MIRTH_FRAME != null && PlatformUI.MIRTH_FRAME.logout(true))); + return (LoginPanelFactory.getLoginPanel().isVisible() || (PlatformUI.MIRTH_FRAME != null && PlatformUI.MIRTH_FRAME.logout(true))); } /** @@ -278,8 +292,153 @@ private static void start(final String server, final String version, final Strin public void run() { initUIManager(); PlatformUI.BACKGROUND_IMAGE = new ImageIcon(com.mirth.connect.client.ui.Frame.class.getResource("images/header_nologo.png")); - LoginPanel.getInstance().initialize(server, version, username, password); + showLogin(username, password); } }); } + + static void showLogin() { + showLogin("", ""); + } + + static void showLogin(String user, String pass) { + LoginPanelFactory.getLoginPanel().initialize( + PlatformUI.SERVER_URL, PlatformUI.CLIENT_VERSION, user, pass, + Mirth::handleLoginSuccess); + } + + private static boolean handleLoginSuccess(Client client, LoginStatus loginStatus, String userName) throws ClientException { + LoginPanel loginPanel = LoginPanelFactory.getLoginPanel(); + try { + PublicServerSettings publicServerSettings = client.getPublicServerSettings(); + + if (publicServerSettings.getLoginNotificationEnabled() == true) { + boolean isAccepted = loginPanel.showLoginNotification("Login Notification", publicServerSettings.getLoginNotificationMessage()); + + if (isAccepted == true) { + client.setUserNotificationAcknowledged(client.getCurrentUser().getId()); + } + else { + return false; + } + } + + String environmentName = publicServerSettings.getEnvironmentName(); + if (!StringUtils.isBlank(environmentName)) { + PlatformUI.ENVIRONMENT_NAME = environmentName; + } + + String serverName = publicServerSettings.getServerName(); + if (!StringUtils.isBlank(serverName)) { + PlatformUI.SERVER_NAME = serverName; + } else { + PlatformUI.SERVER_NAME = null; + } + + Color defaultBackgroundColor = publicServerSettings.getDefaultAdministratorBackgroundColor(); + if (defaultBackgroundColor != null) { + PlatformUI.DEFAULT_BACKGROUND_COLOR = defaultBackgroundColor; + } + } catch (ClientException e) { + PlatformUI.SERVER_NAME = null; + } + + try { + String database = (String) client.getAbout().get("database"); + if (!StringUtils.isBlank(database)) { + PlatformUI.SERVER_DATABASE = database; + } else { + PlatformUI.SERVER_DATABASE = null; + } + } catch (ClientException e) { + PlatformUI.SERVER_DATABASE = null; + } + + try { + Map map = client.getProtocolsAndCipherSuites(); + PlatformUI.SERVER_HTTPS_SUPPORTED_PROTOCOLS = map.get(MirthSSLUtil.KEY_SUPPORTED_PROTOCOLS); + PlatformUI.SERVER_HTTPS_ENABLED_CLIENT_PROTOCOLS = map.get(MirthSSLUtil.KEY_ENABLED_CLIENT_PROTOCOLS); + PlatformUI.SERVER_HTTPS_ENABLED_SERVER_PROTOCOLS = map.get(MirthSSLUtil.KEY_ENABLED_SERVER_PROTOCOLS); + PlatformUI.SERVER_HTTPS_SUPPORTED_CIPHER_SUITES = map.get(MirthSSLUtil.KEY_SUPPORTED_CIPHER_SUITES); + PlatformUI.SERVER_HTTPS_ENABLED_CIPHER_SUITES = map.get(MirthSSLUtil.KEY_ENABLED_CIPHER_SUITES); + } catch (ClientException e) { + } + + PlatformUI.USER_NAME = StringUtils.defaultString(loginStatus.getUpdatedUsername(), userName); + loginPanel.setStatus("Authenticated..."); + new Mirth(client); + loginPanel.setVisible(false); + + User currentUser = PlatformUI.MIRTH_FRAME.getCurrentUser(PlatformUI.MIRTH_FRAME); + Properties userPreferences = new Properties(); + Set preferenceNames = new HashSet(); + preferenceNames.add("firstlogin"); + preferenceNames.add("checkForNotifications"); + preferenceNames.add("showNotificationPopup"); + preferenceNames.add("archivedNotifications"); + try { + userPreferences = client.getUserPreferences(currentUser.getId(), preferenceNames); + + // Display registration dialog if it's the user's first time logging in + String firstlogin = userPreferences.getProperty("firstlogin"); + if (firstlogin == null || BooleanUtils.toBoolean(firstlogin)) { + if (Integer.valueOf(currentUser.getId()) == 1) { + // if current user is user 1: + // 1. check system preferences for user information + // 2. if system preferences exist, populate screen using currentUser + Preferences preferences = Preferences.userNodeForPackage(Mirth.class); + String systemUserInfo = preferences.get("userLoginInfo", null); + if (systemUserInfo != null) { + String info[] = systemUserInfo.split(",", 0); + currentUser.setUsername(info[0]); + currentUser.setFirstName(info[1]); + currentUser.setLastName(info[2]); + currentUser.setEmail(info[3]); + currentUser.setCountry(info[4]); + currentUser.setStateTerritory(info[5]); + currentUser.setPhoneNumber(info[6]); + currentUser.setOrganization(info[7]); + currentUser.setRole(info[8]); + currentUser.setIndustry(info[9]); + currentUser.setDescription(info[10]); + } + } + FirstLoginDialog firstLoginDialog = new FirstLoginDialog(currentUser); + // if leaving the first login dialog without saving + if (!firstLoginDialog.getResult()) { + return false; + } + } else if (loginStatus.getStatus() == LoginStatus.Status.SUCCESS_GRACE_PERIOD) { + new ChangePasswordDialog(currentUser, loginStatus.getMessage()); + } + + // Check for new notifications from update server if enabled + String checkForNotifications = userPreferences.getProperty("checkForNotifications"); + if (CHECK_FOR_NOTIFICATIONS + && (checkForNotifications == null || BooleanUtils.toBoolean(checkForNotifications))) { + Set archivedNotifications = new HashSet(); + String archivedNotificationString = userPreferences.getProperty("archivedNotifications"); + if (archivedNotificationString != null) { + archivedNotifications = ObjectXMLSerializer.getInstance().deserialize(archivedNotificationString, Set.class); + } + // Update the Other Tasks pane with the unarchived notification count + int unarchivedNotifications = ConnectServiceUtil.getNotificationCount(PlatformUI.SERVER_ID, PlatformUI.SERVER_VERSION, LoadedExtensions.getInstance().getExtensionVersions(), archivedNotifications, PlatformUI.HTTPS_PROTOCOLS, PlatformUI.HTTPS_CIPHER_SUITES); + PlatformUI.MIRTH_FRAME.updateNotificationTaskName(unarchivedNotifications); + + // Display notification dialog if enabled and if there are new notifications + String showNotificationPopup = userPreferences.getProperty("showNotificationPopup"); + if (showNotificationPopup == null || BooleanUtils.toBoolean(showNotificationPopup)) { + if (unarchivedNotifications > 0) { + new NotificationDialog(); + } + } + } + } catch (ClientException e) { + PlatformUI.MIRTH_FRAME.alertThrowable(PlatformUI.MIRTH_FRAME, e); + } + + PlatformUI.MIRTH_FRAME.sendUsageStatistics(); + + return true; + } }