OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
182 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.ui.login;
15
 
16
import org.openconcerto.ui.DefaultGridBagConstraints;
17
import org.openconcerto.ui.ReloadPanel;
18
import org.openconcerto.ui.SwingThrottle;
19
import org.openconcerto.ui.component.combo.ISearchableCombo;
20
import org.openconcerto.ui.valuewrapper.EmptyValueWrapper;
21
import org.openconcerto.ui.valuewrapper.ValueWrapperFactory;
22
import org.openconcerto.utils.Base64;
23
import org.openconcerto.utils.JImage;
24
import org.openconcerto.utils.i18n.TM;
25
import org.openconcerto.utils.i18n.TranslationManager;
26
import org.openconcerto.utils.jsonrpc.JSONRPCClient;
27
import org.openconcerto.utils.text.SimpleDocumentListener;
28
 
29
import java.awt.Color;
30
import java.awt.GridBagConstraints;
31
import java.awt.GridBagLayout;
32
import java.awt.Insets;
33
import java.awt.event.ActionEvent;
34
import java.awt.event.ActionListener;
35
import java.beans.PropertyChangeEvent;
36
import java.beans.PropertyChangeListener;
37
import java.io.IOException;
38
import java.security.MessageDigest;
39
import java.security.NoSuchAlgorithmException;
40
import java.util.ArrayList;
41
import java.util.Arrays;
42
import java.util.Collections;
43
import java.util.List;
44
import java.util.Locale;
45
import java.util.ResourceBundle;
46
import java.util.concurrent.ExecutorService;
47
import java.util.concurrent.Executors;
48
 
49
import javax.swing.JButton;
50
import javax.swing.JCheckBox;
51
import javax.swing.JCheckBoxMenuItem;
52
import javax.swing.JFrame;
53
import javax.swing.JLabel;
54
import javax.swing.JPanel;
55
import javax.swing.JPasswordField;
56
import javax.swing.JPopupMenu;
57
import javax.swing.JSeparator;
58
import javax.swing.JTextField;
59
import javax.swing.SwingConstants;
60
import javax.swing.SwingUtilities;
61
import javax.swing.event.DocumentEvent;
62
 
63
import net.minidev.json.JSONArray;
64
import net.minidev.json.JSONObject;
65
 
66
public class LoginFrame extends JFrame implements ActionListener, ConnectionStateListener {
67
 
68
    public static final int CONNECTION_NOT_INITIATED = -1;
69
    public static final int CONNECTION_INPROGRESS = 0;
70
    public static final int CONNECTION_OK = 1;
71
    public static final int CONNECTION_TIMEOUT = 2;
72
    public static final int CONNECTION_REFUSED = 3;
73
    public static final int CONNECTION_BROKEN = 4;
74
    private final LoginProperties loginProperties;
75
 
76
    // Login
77
    private EmptyValueWrapper<String> textLogin;
78
    // Password
79
    private JPasswordField textPassWord;
80
    private String encryptedPassword;
81
    private String clearPassword;
82
    private boolean allowStoredPass = true;
83
 
84
    // Company
85
    private ISearchableCombo<Company> comboCompany;
86
 
87
    private ReloadPanel reloadPanel;
88
 
89
    private CompanyIListModel model;
90
 
91
    // 1/ open an empty interface, rotate reloadpanel
92
    // 2/ fill with stored login/password/ defaultcompany (id+name)
93
    // 3a/ when connection to server is ready, stop reloadpanel
94
    // 3b/ when login+pass are entered, retrieve allowed companies and enable
95
    // connect when
96
    // connection to server is ready
97
 
98
    private final JCheckBox saveCheckBox = new JCheckBox(getTM().translate("loginPanel.storePass"));
99
    private final JButton buttonConnect = new JButton(getTM().translate("loginPanel.loginAction"));
100
 
101
    private final JLabel loginLabel = new JLabel(getTM().translate("loginPanel.loginLabel"));
102
    private final JLabel passwordLabel = new JLabel(getTM().translate("loginPanel.passLabel"));
103
    private final JLabel companyLabel = new JLabel(getTM().translate("loginPanel.companyLabel"));
104
    private String localeBaseName = null;
105
    private final List<Locale> localesToDisplay = new ArrayList<Locale>();
106
    private final JButton langButton = new JButton(Locale.ROOT.getLanguage());
107
    private String company;
108
    private final JSONRPCClient client;
109
    private int connectionState = CONNECTION_NOT_INITIATED;
110
    private final ExecutorService executorService = Executors.newFixedThreadPool(1);
111
    private final String serverUrl;
112
    private String context;
113
    private int idCompany;
114
    private SwingThrottle tCheckValidity;
115
 
116
    public LoginFrame(String serverUrl, final JSONRPCClient client, String context) throws IOException {
117
        this.client = client;
118
        this.serverUrl = serverUrl;
119
        this.context = context;
120
        this.setContentPane(createLoginPanel());
121
        // load stored info
122
        System.err.println("LoginFrame.LoginFrame()" + serverUrl);
123
        this.loginProperties = new LoginProperties(serverUrl);
124
        this.loginProperties.load();
125
 
126
        final Company storedDefaultCompany = new Company(this.loginProperties.getLastCompanyId(), this.loginProperties.getLastCompanyName());
127
        this.init(this.loginProperties.getLastLoginName(), this.loginProperties.getEncryptedStoredPassword(), storedDefaultCompany);
128
 
129
    }
130
 
131
    public synchronized int getConnectionState() {
132
        return this.connectionState;
133
    }
134
 
135
    public synchronized void setConnectionState(int connectionState) {
136
        this.connectionState = connectionState;
137
    }
138
 
139
    private TM getTM() {
140
        return org.openconcerto.utils.i18n.TM.getInstance();
141
    }
142
 
143
    private void init(String storedLogin, String storedEncryptedPassword, Company storedDefaultCompany) {
144
        this.textLogin.setValue(storedLogin);
145
 
146
        if (this.saveCheckBox != null && storedEncryptedPassword != null && storedEncryptedPassword.length() > 0) {
147
            this.saveCheckBox.setSelected(true);
148
        }
149
        // to show the user its password has been retrieved
150
        if (storedEncryptedPassword != null) {
151
            final char[] s = new char[8];
152
            Arrays.fill(s, ' ');
153
            this.textPassWord.setText(new String(s));
154
            this.clearPassword = null;
155
        } else
156
            this.clearPassword = "";
157
 
158
        this.model = new CompanyIListModel();
159
        this.comboCompany.initCache(this.model);
160
 
161
        this.tCheckValidity = new SwingThrottle(800, new Runnable() {
162
 
163
            @Override
164
            public void run() {
165
                checkValidity();
166
            }
167
 
168
        });
169
    }
170
 
171
    private JPanel createLoginPanel() {
172
        JPanel panel = new JPanel();
173
 
174
        panel.setLayout(new GridBagLayout());
175
 
176
        final GridBagConstraints c = new GridBagConstraints();
177
        c.gridheight = 1;
178
        c.gridwidth = 2;
179
        c.gridx = 0;
180
        c.gridy = 0;
181
        c.weightx = 1;
182
        c.weighty = 0;
183
        c.insets = new Insets(0, 0, 0, 0);
184
        c.fill = GridBagConstraints.HORIZONTAL;
185
 
186
        // Logo
187
 
188
        JImage imageLogo = new JImage(LoginFrame.class.getResource("OpenConcerto_login.png"));
189
 
190
        imageLogo.setBackground(Color.WHITE);
191
        imageLogo.check();
192
        panel.add(imageLogo, c);
193
        c.gridy++;
194
        c.gridwidth = GridBagConstraints.REMAINDER;
195
        panel.add(new JSeparator(SwingConstants.HORIZONTAL), c);
196
 
197
        // Login
198
        c.insets = new Insets(2, 2, 1, 2);
199
        c.gridy++;
200
        c.gridwidth = 1;
201
        c.weightx = 0;
202
 
203
        this.loginLabel.setHorizontalAlignment(SwingConstants.RIGHT);
204
        panel.add(this.loginLabel, c);
205
 
206
        this.textLogin = new EmptyValueWrapper<String>(ValueWrapperFactory.create(new JTextField(), String.class));
207
 
208
        c.gridx++;
209
        c.weightx = 1;
210
        panel.add(this.textLogin.getComp(), c);
211
        ((JTextField) this.textLogin.getComp()).addActionListener(this);
212
        this.textLogin.addValueListener(new PropertyChangeListener() {
213
            public void propertyChange(final PropertyChangeEvent evt) {
214
                LoginFrame.this.tCheckValidity.execute();
215
            }
216
        });
217
 
218
        // Password
219
        c.gridy++;
220
        c.gridx = 0;
221
        c.weightx = 0;
222
        System.err.println("LoginFrame.createLoginPanel()22c");
223
        this.passwordLabel.setHorizontalAlignment(SwingConstants.RIGHT);
224
        panel.add(this.passwordLabel, c);
225
 
226
        this.textPassWord = new JPasswordField();
227
 
228
        c.gridx++;
229
        c.weightx = 1;
230
        panel.add(this.textPassWord, c);
231
        this.textPassWord.getDocument().addDocumentListener(new SimpleDocumentListener() {
232
            public void update(final DocumentEvent e) {
233
                LoginFrame.this.clearPassword = String.valueOf(LoginFrame.this.textPassWord.getPassword());
234
                LoginFrame.this.tCheckValidity.execute();
235
            }
236
        });
237
        this.textPassWord.addActionListener(this);
238
        System.err.println("LoginFrame.createLoginPanel()22d");
239
        // Societe
240
 
241
        c.gridy++;
242
        c.gridx = 0;
243
        c.weightx = 0;
244
 
245
        this.companyLabel.setHorizontalAlignment(SwingConstants.RIGHT);
246
        panel.add(this.companyLabel, c);
247
 
248
        this.comboCompany = new ISearchableCombo<Company>(true);
249
        this.comboCompany.setEnabled(false);
250
 
251
        c.gridx++;
252
        panel.add(this.comboCompany, c);
253
 
254
        // Button
255
        final JPanel panelButton = new JPanel();
256
        panelButton.setOpaque(false);
257
        panelButton.setLayout(new GridBagLayout());
258
        final GridBagConstraints c2 = new DefaultGridBagConstraints();
259
        c2.weightx = 1;
260
        if (this.allowStoredPass) {
261
 
262
            this.saveCheckBox.setOpaque(false);
263
            panelButton.add(this.saveCheckBox, c2);
264
            c2.weightx = 0;
265
 
266
        }
267
        c2.gridx++;
268
 
269
        // Language selector
270
        //
271
        this.langButton.addActionListener(new ActionListener() {
272
 
273
            @Override
274
            public void actionPerformed(ActionEvent e) {
275
                final JPopupMenu menu = new JPopupMenu();
276
                final Locale locale = Locale.getDefault();
277
                for (final Locale l : LoginFrame.this.localesToDisplay) {
278
                    final JCheckBoxMenuItem menuItem = new JCheckBoxMenuItem(l.getDisplayName(l));
279
                    if (l.equals(locale)) {
280
                        menuItem.setSelected(true);
281
                    }
282
                    menu.add(menuItem);
283
                    menuItem.addActionListener(new ActionListener() {
284
                        @Override
285
                        public void actionPerformed(ActionEvent e) {
286
                            setUILanguage(l);
287
                        }
288
                    });
289
                }
290
 
291
                menu.show(LoginFrame.this.langButton, 0, 0);
292
 
293
            }
294
        });
295
        this.langButton.setOpaque(false);
296
        this.langButton.setBorderPainted(false);
297
        this.langButton.setContentAreaFilled(false);
298
        this.langButton.setBorder(null);
299
        this.langButton.setFocusable(false);
300
        this.langButton.setVisible(false);
301
        panelButton.add(this.langButton, c2);
302
        c2.gridx++;
303
        this.reloadPanel = new ReloadPanel();
304
        this.reloadPanel.setOpaque(false);
305
 
306
        this.reloadPanel.setMode(ReloadPanel.MODE_EMPTY);
307
        panelButton.add(this.reloadPanel, c2);
308
        c2.gridx++;
309
        c2.weightx = 0;
310
        this.buttonConnect.setEnabled(false);
311
        this.buttonConnect.setOpaque(false);
312
        panelButton.add(this.buttonConnect, c2);
313
 
314
        c.gridy++;
315
        c.gridx = 0;
316
        c.weightx = 0;
317
        c.gridwidth = GridBagConstraints.REMAINDER;
318
        c.weighty = 1;
319
        panel.add(panelButton, c);
320
 
321
        this.buttonConnect.addActionListener(this);
322
 
323
        initLocalization("org.openconcerto.ui.login.Messages",
324
                Arrays.asList(Locale.FRANCE, Locale.CANADA_FRENCH, new Locale("fr", "CH"), new Locale("fr", "BE"), Locale.UK, Locale.CANADA, Locale.US, Locale.GERMANY, new Locale("de", "CH")));
325
 
326
        setUILanguage(Locale.getDefault());
327
 
328
        return panel;
329
    }
330
 
331
    private void checkValidity() {
332
        String login = this.textLogin.getValue();
333
        this.buttonConnect.setEnabled(login != null && !login.trim().isEmpty() && this.comboCompany.getSelectedItem() != null);
334
 
335
        // this.buttonConnect.setEnabled(this.connectionAllowed == null &&
336
        // this.areFieldsValidated());
337
        // this.buttonConnect.setToolTipText(this.connectionAllowed);
338
        System.err.println("LoginFrame.checkValidity()");
339
        String ePassword = this.encryptedPassword;
340
        if (ePassword == null) {
341
            if (this.clearPassword == null) {
342
                this.clearPassword = "";
343
            }
344
            ePassword = encodePassword(this.clearPassword);
345
        }
346
        // Empty login is not allowed
347
        if (login == null || login.trim().isEmpty()) {
348
            return;
349
        }
350
        this.reloadPanel.setMode(ReloadPanel.MODE_ROTATE);
351
        try {
352
            sendAllowedCompanyRequest(login, ePassword);
353
        } catch (Exception e) {
354
            // Connection to server impossible
355
            this.reloadPanel.setMode(ReloadPanel.MODE_BLINK);
356
            e.printStackTrace();
357
        }
358
    }
359
 
360
    @Override
361
    public void actionPerformed(ActionEvent e) {
362
        if (this.comboCompany.getSelectedItem() == null) {
363
            return;
364
        }
365
 
366
        // Connect
367
        this.setEnabled(false);
368
        this.reloadPanel.setMode(ReloadPanel.MODE_ROTATE);
369
        String ePassword = this.encryptedPassword;
370
        if (ePassword == null) {
371
            if (this.clearPassword == null) {
372
                this.clearPassword = "";
373
            }
374
            ePassword = encodePassword(this.clearPassword);
375
        }
376
        this.company = this.comboCompany.getSelectedItem().getName();
377
        sendLoginRequest(this.textLogin.getValue(), ePassword, this.comboCompany.getSelectedItem().getId(), this.context);
378
 
379
    }
380
 
381
    @Override
382
    public void stateChanged(final int oldState, final int newState) {
383
        if (SwingUtilities.isEventDispatchThread()) {
384
            throw new IllegalAccessError("Must not be called in EventDispatchThread");
385
        }
386
 
387
        SwingUtilities.invokeLater(new Runnable() {
388
 
389
            @Override
390
            public void run() {
391
                if (newState == CONNECTION_REFUSED || newState == CONNECTION_TIMEOUT) {
392
                    LoginFrame.this.reloadPanel.setMode(ReloadPanel.MODE_BLINK);
393
                } else if (newState == CONNECTION_OK) {
394
                    LoginFrame.this.reloadPanel.setMode(ReloadPanel.MODE_EMPTY);
395
                } else {
396
                    LoginFrame.this.reloadPanel.setMode(ReloadPanel.MODE_BLINK);
397
                    // Connection to server lost
398
                    // FIXME: autoreconnect after 10s
399
                }
400
 
401
            }
402
        });
403
 
404
    }
405
 
406
    @Override
407
    public void setAllowedCompanies(final List<Company> list) {
408
        if (SwingUtilities.isEventDispatchThread()) {
409
            throw new IllegalAccessError("Must not be called in EventDispatchThread");
410
        }
411
 
412
        SwingUtilities.invokeLater(new Runnable() {
413
 
414
            @Override
415
            public void run() {
416
 
417
                LoginFrame.this.model.updateFrom(list);
418
                if (!list.isEmpty()) {
419
                    LoginFrame.this.comboCompany.setEnabled(true);
420
                    for (Company company : list) {
421
                        // Reselect from saved company
422
                        if (company.getId() == LoginFrame.this.loginProperties.getLastCompanyId()) {
423
                            LoginFrame.this.comboCompany.setValue(company);
424
                            break;
425
                        }
426
                    }
427
 
428
                    if (LoginFrame.this.comboCompany.getSelectedItem() == null) {
429
                        LoginFrame.this.comboCompany.setSelectedIndex(0);
430
                    }
431
                    LoginFrame.this.buttonConnect.setEnabled(!LoginFrame.this.textLogin.getValue().trim().isEmpty() && LoginFrame.this.comboCompany.getSelectedItem() != null);
432
                } else {
433
                    LoginFrame.this.buttonConnect.setEnabled(false);
434
                }
435
                LoginFrame.this.reloadPanel.setMode(ReloadPanel.MODE_EMPTY);
436
            }
437
        });
438
    }
439
 
440
    @Override
441
    public void setEnabled(boolean b) {
442
        if (b) {
443
            this.reloadPanel.setMode(ReloadPanel.MODE_EMPTY);
444
        }
445
        this.textLogin.getComp().setEnabled(b);
446
        this.textPassWord.setEnabled(b);
447
        this.comboCompany.setEnabled(b);
448
        this.saveCheckBox.setEnabled(b);
449
        this.buttonConnect.setEnabled(b);
450
    }
451
 
452
    private final void initLocalization(final String baseName, final List<Locale> toDisplay) {
453
        if (baseName == null)
454
            throw new NullPointerException("Null baseName");
455
        if (this.localeBaseName != null)
456
            throw new IllegalStateException("Already inited to " + this.localeBaseName);
457
        this.localeBaseName = baseName;
458
        this.localesToDisplay.addAll(toDisplay);
459
        // this.setUILanguage(UserProps.getInstance().getLocale());
460
        TM.getInstance();
461
    }
462
 
463
    private void setUILanguage(Locale locale) {
464
        System.err.println("LoginFrame.setUILanguage():" + this.localeBaseName + " locale:" + locale);
465
        final ResourceBundle bundle = ResourceBundle.getBundle(this.localeBaseName, locale, TranslationManager.getControl());
466
        this.loginLabel.setText(bundle.getString("loginLabel"));
467
        this.passwordLabel.setText(bundle.getString("passwordLabel"));
468
        this.companyLabel.setText(bundle.getString("companyLabel"));
469
        this.saveCheckBox.setText(bundle.getString("saveCheckBox"));
470
        this.buttonConnect.setText(bundle.getString("buttonConnect"));
471
        this.langButton.setText(locale.getLanguage());
472
        this.langButton.setVisible(true);
473
        Locale.setDefault(locale);
474
    }
475
 
476
    private static MessageDigest md;
477
 
478
    private static synchronized MessageDigest getMessageDigest() {
479
        if (md == null)
480
            try {
481
                md = MessageDigest.getInstance("SHA-1");
482
            } catch (NoSuchAlgorithmException e) {
483
                // should be standard
484
                throw new IllegalStateException("no SHA1", e);
485
            }
486
        return md;
487
    }
488
 
489
    public static String encodePassword(String clearPassword) {
490
        final byte[] s;
491
        synchronized (getMessageDigest()) {
492
            getMessageDigest().reset();
493
            getMessageDigest().update(clearPassword.getBytes());
494
            s = getMessageDigest().digest();
495
        }
496
        return Base64.encodeBytes(s);
497
    }
498
 
499
    public String getCompany() {
500
        return this.company;
501
    }
502
 
503
    public void saveAndDispose() throws IOException {
504
        this.loginProperties.setLastCompanyId(this.comboCompany.getSelectedItem().getId());
505
        this.loginProperties.setLastCompanyName(this.comboCompany.getSelectedItem().getName());
506
        this.loginProperties.setLastLoginName(this.textLogin.getValue());
507
        this.loginProperties.store();
508
        dispose();
509
    }
510
 
511
    private void sendAllowedCompanyRequest(String login, String passwordHash) {
512
        this.executorService.execute(new Runnable() {
513
 
514
            @Override
515
            public void run() {
516
                LoginFrame.this.client.setCredentials(login, passwordHash);
517
                final JSONObject params = new JSONObject();
518
                params.put("login", login);
519
                try {
520
                    JSONObject result = (JSONObject) LoginFrame.this.client.rpcCall(LoginFrame.this.serverUrl, "getAllowedCompanies", params);
521
                    JSONArray array = (JSONArray) result.get("companies");
522
                    List<Company> list = new ArrayList<>();
523
                    for (Object o : array) {
524
                        final Company c = new Company();
525
                        c.fromJSON((JSONObject) o);
526
                        list.add(c);
527
                    }
528
                    setAllowedCompanies(list);
529
 
530
                } catch (IOException e) {
531
                    setAllowedCompanies(Collections.emptyList());
532
                    e.printStackTrace();
533
                }
534
 
535
            }
536
        });
537
    }
538
 
539
    /**
540
     * @param encodedPassword : Base64 encodded hash of the password
541
     */
542
    private void sendLoginRequest(String login, String encodedPassword, int idCompany, final String context) {
543
        this.executorService.execute(new Runnable() {
544
 
545
            @Override
546
            public void run() {
547
                stateChanged(getConnectionState(), CONNECTION_INPROGRESS);
548
                LoginFrame.this.client.setCredentials(login, encodedPassword);
549
                final JSONObject params = new JSONObject();
550
                params.put("company-id", idCompany);
551
                params.put("context", context);
552
                try {
553
                    JSONObject result = (JSONObject) LoginFrame.this.client.rpcCall(LoginFrame.this.serverUrl, "login", params);
554
                    if (result.getAsString("status").equals("granted")) {
555
                        LoginFrame.this.idCompany = idCompany;
556
                        stateChanged(getConnectionState(), CONNECTION_OK);
557
                    } else {
558
                        stateChanged(getConnectionState(), CONNECTION_REFUSED);
559
                    }
560
                } catch (IOException e) {
561
                    stateChanged(getConnectionState(), CONNECTION_BROKEN);
562
                }
563
 
564
            }
565
        });
566
 
567
    }
568
 
569
    public int getCompanyId() {
570
        return this.idCompany;
571
    }
572
}