Project

General

Profile

Download (14 KB) Statistics
| Branch: | Tag: | Revision:
1
/**
2
* Copyright (C) 2017 EDIT
3
* European Distributed Institute of Taxonomy
4
* http://www.e-taxonomy.eu
5
*
6
* The contents of this file are subject to the Mozilla Public License Version 1.1
7
* See LICENSE.TXT at the top of this package for the full license terms.
8
*/
9
package eu.etaxonomy.cdm.vaadin.view;
10

    
11
import java.net.MalformedURLException;
12
import java.net.URL;
13
import java.util.ArrayList;
14
import java.util.List;
15
import java.util.concurrent.CountDownLatch;
16
import java.util.concurrent.ExecutionException;
17
import java.util.concurrent.TimeUnit;
18

    
19
import javax.mail.internet.AddressException;
20

    
21
import org.apache.commons.lang.StringUtils;
22
import org.apache.log4j.Logger;
23
import org.springframework.beans.factory.annotation.Autowired;
24
import org.springframework.beans.factory.annotation.Qualifier;
25
import org.springframework.core.env.Environment;
26
import org.springframework.mail.MailException;
27
import org.springframework.security.authentication.AuthenticationManager;
28
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
29
import org.springframework.security.core.Authentication;
30
import org.springframework.security.core.AuthenticationException;
31
import org.springframework.util.concurrent.ListenableFuture;
32
import org.vaadin.spring.events.Event;
33
import org.vaadin.spring.events.EventBus;
34
import org.vaadin.spring.events.EventBusListener;
35
import org.vaadin.spring.events.annotation.EventBusListenerMethod;
36

    
37
import com.vaadin.data.validator.AbstractStringValidator;
38
import com.vaadin.spring.annotation.SpringComponent;
39
import com.vaadin.spring.annotation.ViewScope;
40
import com.vaadin.ui.themes.ValoTheme;
41

    
42
import eu.etaxonomy.cdm.api.application.ICdmRepository;
43
import eu.etaxonomy.cdm.api.config.CdmConfigurationKeys;
44
import eu.etaxonomy.cdm.api.service.security.AccountSelfManagementException;
45
import eu.etaxonomy.cdm.api.service.security.EmailAddressNotFoundException;
46
import eu.etaxonomy.cdm.vaadin.event.AuthenticationAttemptEvent;
47
import eu.etaxonomy.cdm.vaadin.event.AuthenticationSuccessEvent;
48
import eu.etaxonomy.cdm.vaadin.event.UserAccountEvent;
49
import eu.etaxonomy.cdm.vaadin.ui.UserAccountSelfManagementUI;
50
import eu.etaxonomy.cdm.vaadin.util.VaadinServletUtilities;
51
import eu.etaxonomy.vaadin.mvp.AbstractPresenter;
52
import eu.etaxonomy.vaadin.ui.navigation.NavigationEvent;
53
import eu.etaxonomy.vaadin.ui.navigation.NavigationManager;
54

    
55
/**
56
 * The {@link LoginView} is used as replacement view in the scope of other views.
57
 * Therefore the LoginPresenter must be in <b>UIScope</b> so that the LoginPresenter
58
 * is available to all Views.
59
 * <p>
60
 * The LoginPresenter offers a <b>auto login feature for developers</b>. To activate the auto login
61
 * you need to provide the <code>user name</code> and <code>password</code> using the environment variables
62
 * <code>cdm-vaadin.login.usr</code> and <code>cdm-vaadin.login.pwd</code>, e.g.:
63
 * <pre>
64
 * -Dcdm-vaadin.login.usr=admin -Dcdm-vaadin.login.pwd=00000
65
 * </pre>
66
 *
67
 * @author a.kohlbecker
68
 * @since Apr 25, 2017
69
 *
70
 */
71
@SpringComponent
72
@ViewScope
73
public class LoginPresenter extends AbstractPresenter<LoginView> implements EventBusListener<AuthenticationAttemptEvent> {
74

    
75
    private static final long serialVersionUID = 4020699735656994791L;
76

    
77
    private static final Logger log = Logger.getLogger(LoginPresenter.class);
78

    
79
    private final static String PROPNAME_USER = "cdm-vaadin.login.usr";
80

    
81
    private final static String PROPNAME_PASSWORD = "cdm-vaadin.login.pwd";
82

    
83
    private String redirectToState;
84

    
85
    protected EventBus.UIEventBus uiEventBus;
86

    
87
    @Autowired
88
    @Qualifier("cdmRepository")
89
    private ICdmRepository repo;
90

    
91
    @Autowired
92
    protected Environment env;
93

    
94
//    @Override
95
//    protected void eventViewBusSubscription(ViewEventBus viewEventBus) {
96
//        viewEventBus.subscribe(this);
97
//    }
98

    
99
    @Autowired
100
    protected void setUIEventBus(EventBus.UIEventBus uiEventBus){
101
        this.uiEventBus = uiEventBus;
102
        uiEventBus.subscribe(this);
103
    }
104

    
105
    public boolean authenticate(String userName, String password) {
106

    
107
        getView().clearMessage();
108

    
109
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userName, password);
110
        AuthenticationManager authenticationManager = getRepo().getAuthenticationManager();
111
        try {
112
            Authentication authentication = authenticationManager.authenticate(token);
113
            if(authentication != null && authentication.isAuthenticated()) {
114
                log.debug("user '" + userName + "' authenticated");
115
                currentSecurityContext().setAuthentication(authentication);
116
                if(NavigationManager.class.isAssignableFrom(getNavigationManager().getClass())){
117
                    uiEventBus.publish(this, new AuthenticationSuccessEvent(userName));
118
                    log.debug("redirecting to " + redirectToState);
119
                    uiEventBus.publish(this, new NavigationEvent(redirectToState));
120
                }
121
            }
122
        } catch (AuthenticationException e){
123
            getView().showErrorMessage("Login failed! Please check your username and password.");
124
        }
125
        return false;
126
    }
127

    
128
    @Override
129
    public void handleViewEntered() {
130

    
131
        List<String> redirectToStateTokens = getNavigationManager().getCurrentViewParameters();
132
        String currentViewName = getNavigationManager().getCurrentViewName();
133

    
134
        if(currentViewName.equals(LoginViewBean.NAME) && redirectToStateTokens.isEmpty()){
135
            // login view is shown in turn to an explicit login request of the user (e.g. login button pressed)
136
            // use the redirectToStateTokens 1-n as redirectToState
137
            //FIXME implement : redirectToState = UserView.NAME
138

    
139
        } else {
140
            // the login view is shown instead of the requested view for which the user needs to login
141
            redirectToState = String.join("/", redirectToStateTokens);
142
        }
143

    
144
        getView().getLoginDialog().getEmail().addValidator(new AbstractStringValidator("An account for this email address already exits. You may want to use the \"Password Revovery\" tab intsead?") {
145
            private static final long serialVersionUID = 1L;
146
            @Override
147
            protected boolean isValidValue(String value) {
148
                return !repo.getAccountRegistrationService().emailAddressExists(value);
149
            }
150
        });
151

    
152
        // attempt to auto login
153
        if(StringUtils.isNotEmpty(System.getProperty(PROPNAME_USER)) && StringUtils.isNotEmpty(System.getProperty(PROPNAME_PASSWORD))){
154
            log.warn("Performing autologin with user " + System.getProperty(PROPNAME_USER));
155
            authenticate(System.getProperty(PROPNAME_USER), System.getProperty(PROPNAME_PASSWORD));
156
        }
157

    
158
    }
159

    
160
    @Override
161
    public void onEvent(Event<AuthenticationAttemptEvent> event) {
162
        if(getView()!= null){
163
            authenticate(event.getPayload().getUserName(), getView().getLoginDialog().getPassword().getValue());
164
        } else {
165
            log.info("view is NULL, not yet disposed LoginPresenter?");
166
        }
167
    }
168

    
169
    @EventBusListenerMethod
170
    public void onPasswordRevoveryEvent(UserAccountEvent event) throws MalformedURLException, ExecutionException, MailException, AddressException, AccountSelfManagementException {
171

    
172
        if(event.getAction().equals(UserAccountEvent.UserAccountAction.REQUEST_PASSWORD_RESET)) {
173
            requestPasswordReset();
174
        } else if(event.getAction().equals(UserAccountEvent.UserAccountAction.REGISTER_ACCOUNT)) {
175
            requestAccountCreation();
176
        }
177
    }
178

    
179
    private void requestPasswordReset() throws MalformedURLException, ExecutionException {
180
        String userNameOrEmail = getView().getLoginDialog().getUserNameOrEmail().getValue();
181
        URL servletBaseUrl = VaadinServletUtilities.getServletBaseUrl();
182
        logger.debug("UserAccountAction.REQUEST_PASSWORD_RESET for " + servletBaseUrl + ", userNameOrEmail:" + userNameOrEmail);
183
        // Implementation note: UI modifications allied in the below callback methods will not affect the UI
184
        // immediately, therefore we use a CountDownLatch
185
        CountDownLatch finshedSignal = new CountDownLatch(1);
186
        List<Throwable> asyncException = new ArrayList<>(1);
187
        ListenableFuture<Boolean> futureResult = repo.getPasswordResetService().emailResetToken(
188
                userNameOrEmail,
189
                servletBaseUrl.toString() + "/app/" + UserAccountSelfManagementUI.NAME + "#!" + PasswordResetViewBean.NAME + "/%s");
190
        futureResult.addCallback(
191
                    successFuture -> {
192
                        finshedSignal.countDown();
193
                    },
194
                    exception -> {
195
                        // possible MailException
196
                        asyncException.add(exception);
197
                        finshedSignal.countDown();
198
                    }
199
                );
200
        boolean asyncTimeout = false;
201
        Boolean result = false;
202
        try {
203
            finshedSignal.await(2, TimeUnit.SECONDS);
204
            result = futureResult.get();
205
        } catch (InterruptedException e) {
206
            asyncTimeout = true;
207
        } catch (Exception e) {
208
            // in case executing getUserNameOrEmail() causes an exception faster
209
            // than futureResult.addCallback( can be processed, the exception
210
            // can not be caught asynchronously
211
            // so we are adding all these exceptions here
212
            asyncException.add(e);
213
        }
214
        if(!asyncException.isEmpty()) {
215
            String messageText = "An unknown error has occurred.";
216
            if(asyncException.get(0) instanceof MailException) {
217
                String supportText = "the support";
218
                String supportEmail = env.getProperty(CdmConfigurationKeys.MAIL_ADDRESS_SUPPORT);
219
                if(supportEmail != null) {
220
                    supportText = "<a href=\"mailto:" + supportEmail +"\">" + supportEmail + "</a>";
221
                }
222
                messageText = "Sending the password reset email to you has failed. Please try again later or contact " + supportText + " in case this error persists.";
223
            }
224
            if(asyncException.get(0) instanceof EmailAddressNotFoundException) {
225
                messageText = "There is no user accout for this email address.";
226
            }
227
            getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue(messageText);
228
            getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme.LABEL_FAILURE);
229
        } else {
230
            if(!asyncTimeout && result) {
231
                getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue("An email with a password reset link has been sent to you.");
232
                getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme.LABEL_SUCCESS);
233
                getView().getLoginDialog().getSendOnetimeLogin().setEnabled(false);
234
                getView().getLoginDialog().getUserNameOrEmail().setEnabled(false);
235
                getView().getLoginDialog().getUserNameOrEmail().setReadOnly(true);
236

    
237
            } else {
238
                getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue("A timeout has occured, please try again.");
239
                getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme.LABEL_FAILURE);
240
            }
241
        }
242
    }
243

    
244
    private void requestAccountCreation() throws MalformedURLException, MailException, AddressException, AccountSelfManagementException, ExecutionException {
245
        String emailAddress = getView().getLoginDialog().getEmail().getValue();
246
        URL servletBaseUrl = VaadinServletUtilities.getServletBaseUrl();
247

    
248
        logger.debug("UserAccountAction.REGISTER_ACCOUNT for " + servletBaseUrl + ", emailAddress:" + emailAddress);
249

    
250
        CountDownLatch finshedSignal = new CountDownLatch(1);
251
        List<Throwable> asyncException = new ArrayList<>(1);
252
        ListenableFuture<Boolean> futureResult = repo.getAccountRegistrationService().emailAccountRegistrationRequest(emailAddress,
253
                servletBaseUrl.toString() + "/app/" + UserAccountSelfManagementUI.NAME + "#!" + AccountRegistrationViewBean.NAME + "/%s");
254
        futureResult.addCallback(
255
                    successFuture -> {
256
                        finshedSignal.countDown();
257
                    },
258
                    exception -> {
259
                        // possible MailException
260
                        asyncException.add(exception);
261
                        finshedSignal.countDown();
262
                    }
263
                );
264
        boolean asyncTimeout = false;
265
        Boolean result = false;
266
        try {
267
            finshedSignal.await(2, TimeUnit.SECONDS);
268
            result = futureResult.get();
269
        } catch (InterruptedException e) {
270
            asyncTimeout = true;
271
        } catch (Exception e) {
272
            // in case executing emailAccountRegistrationRequest() causes an exception faster
273
            // than futureResult.addCallback( can be processed, the exception
274
            // can not be caught asynchronously
275
            // so we are adding all these exceptions here
276
            asyncException.add(e);
277
        }
278
        if(!asyncException.isEmpty()) {
279
            getView().getLoginDialog().getRegisterMessageLabel()
280
            .setValue("Sending the account registration email to you has failed. Please try again later or contect the support in case this error persists.");
281
            getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme.LABEL_FAILURE);
282
        } else {
283
            if(!asyncTimeout && result) {
284
                getView().getLoginDialog().getRegisterMessageLabel().setValue("An email with with further instructions has been sent to you.");
285
                getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme.LABEL_SUCCESS);
286
                getView().getLoginDialog().getEmail().setEnabled(false);
287
                getView().getLoginDialog().getRegisterButton().setEnabled(false);
288

    
289
            } else {
290
                getView().getLoginDialog().getRegisterMessageLabel().setValue("A timeout has occured, please try again.");
291
                getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme.LABEL_FAILURE);
292
            }
293
        }
294
    }
295

    
296
}
(11-11/18)