2 * Copyright (C) 2017 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
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.
9 package eu
.etaxonomy
.cdm
.vaadin
.view
;
11 import java
.net
.MalformedURLException
;
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
;
19 import javax
.mail
.internet
.AddressException
;
21 import org
.apache
.commons
.lang
.StringUtils
;
22 import org
.apache
.logging
.log4j
.LogManager
;
23 import org
.apache
.logging
.log4j
.Logger
;
24 import org
.springframework
.beans
.factory
.annotation
.Autowired
;
25 import org
.springframework
.beans
.factory
.annotation
.Qualifier
;
26 import org
.springframework
.core
.env
.Environment
;
27 import org
.springframework
.mail
.MailException
;
28 import org
.springframework
.security
.authentication
.AuthenticationManager
;
29 import org
.springframework
.security
.authentication
.UsernamePasswordAuthenticationToken
;
30 import org
.springframework
.security
.core
.Authentication
;
31 import org
.springframework
.security
.core
.AuthenticationException
;
32 import org
.springframework
.util
.concurrent
.ListenableFuture
;
33 import org
.vaadin
.spring
.events
.Event
;
34 import org
.vaadin
.spring
.events
.EventBus
;
35 import org
.vaadin
.spring
.events
.EventBusListener
;
36 import org
.vaadin
.spring
.events
.annotation
.EventBusListenerMethod
;
38 import com
.vaadin
.data
.validator
.AbstractStringValidator
;
39 import com
.vaadin
.spring
.annotation
.SpringComponent
;
40 import com
.vaadin
.spring
.annotation
.ViewScope
;
41 import com
.vaadin
.ui
.themes
.ValoTheme
;
43 import eu
.etaxonomy
.cdm
.api
.application
.ICdmRepository
;
44 import eu
.etaxonomy
.cdm
.api
.config
.CdmConfigurationKeys
;
45 import eu
.etaxonomy
.cdm
.api
.service
.security
.AccountSelfManagementException
;
46 import eu
.etaxonomy
.cdm
.api
.service
.security
.EmailAddressNotFoundException
;
47 import eu
.etaxonomy
.cdm
.vaadin
.event
.AuthenticationAttemptEvent
;
48 import eu
.etaxonomy
.cdm
.vaadin
.event
.AuthenticationSuccessEvent
;
49 import eu
.etaxonomy
.cdm
.vaadin
.event
.UserAccountEvent
;
50 import eu
.etaxonomy
.cdm
.vaadin
.ui
.UserAccountSelfManagementUI
;
51 import eu
.etaxonomy
.cdm
.vaadin
.util
.VaadinServletUtilities
;
52 import eu
.etaxonomy
.vaadin
.mvp
.AbstractPresenter
;
53 import eu
.etaxonomy
.vaadin
.ui
.navigation
.NavigationEvent
;
54 import eu
.etaxonomy
.vaadin
.ui
.navigation
.NavigationManager
;
57 * The {@link LoginView} is used as replacement view in the scope of other views.
58 * Therefore the LoginPresenter must be in <b>UIScope</b> so that the LoginPresenter
59 * is available to all Views.
61 * The LoginPresenter offers a <b>auto login feature for developers</b>. To activate the auto login
62 * you need to provide the <code>user name</code> and <code>password</code> using the environment variables
63 * <code>cdm-vaadin.login.usr</code> and <code>cdm-vaadin.login.pwd</code>, e.g.:
65 * -Dcdm-vaadin.login.usr=admin -Dcdm-vaadin.login.pwd=00000
68 * @author a.kohlbecker
73 public class LoginPresenter
extends AbstractPresenter
<LoginView
> implements EventBusListener
<AuthenticationAttemptEvent
> {
75 private static final long serialVersionUID
= 4020699735656994791L;
77 private static final Logger logger
= LogManager
.getLogger();
79 private final static String PROPNAME_USER
= "cdm-vaadin.login.usr";
81 private final static String PROPNAME_PASSWORD
= "cdm-vaadin.login.pwd";
83 private String redirectToState
;
85 protected EventBus
.UIEventBus uiEventBus
;
88 @Qualifier("cdmRepository")
89 private ICdmRepository repo
;
92 protected Environment env
;
95 // protected void eventViewBusSubscription(ViewEventBus viewEventBus) {
96 // viewEventBus.subscribe(this);
100 protected void setUIEventBus(EventBus
.UIEventBus uiEventBus
){
101 this.uiEventBus
= uiEventBus
;
102 uiEventBus
.subscribe(this);
105 public boolean authenticate(String userName
, String password
) {
107 getView().clearMessage();
109 UsernamePasswordAuthenticationToken token
= new UsernamePasswordAuthenticationToken(userName
, password
);
110 AuthenticationManager authenticationManager
= getRepo().getAuthenticationManager();
112 Authentication authentication
= authenticationManager
.authenticate(token
);
113 if(authentication
!= null && authentication
.isAuthenticated()) {
114 logger
.debug("user '" + userName
+ "' authenticated");
115 currentSecurityContext().setAuthentication(authentication
);
116 if(NavigationManager
.class.isAssignableFrom(getNavigationManager().getClass())){
117 uiEventBus
.publish(this, new AuthenticationSuccessEvent(userName
));
118 logger
.debug("redirecting to " + redirectToState
);
119 uiEventBus
.publish(this, new NavigationEvent(redirectToState
));
122 } catch (AuthenticationException e
){
123 getView().showErrorMessage("Login failed! Please check your username and password.");
129 public void handleViewEntered() {
131 List
<String
> redirectToStateTokens
= getNavigationManager().getCurrentViewParameters();
132 String currentViewName
= getNavigationManager().getCurrentViewName();
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
140 // the login view is shown instead of the requested view for which the user needs to login
141 redirectToState
= String
.join("/", redirectToStateTokens
);
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;
147 protected boolean isValidValue(String value
) {
148 return !repo
.getAccountRegistrationService().emailAddressExists(value
);
152 // attempt to auto login
153 if(StringUtils
.isNotEmpty(System
.getProperty(PROPNAME_USER
)) && StringUtils
.isNotEmpty(System
.getProperty(PROPNAME_PASSWORD
))){
154 logger
.warn("Performing autologin with user " + System
.getProperty(PROPNAME_USER
));
155 authenticate(System
.getProperty(PROPNAME_USER
), System
.getProperty(PROPNAME_PASSWORD
));
161 public void onEvent(Event
<AuthenticationAttemptEvent
> event
) {
162 if(getView()!= null){
163 authenticate(event
.getPayload().getUserName(), getView().getLoginDialog().getPassword().getValue());
165 logger
.info("view is NULL, not yet disposed LoginPresenter?");
169 @EventBusListenerMethod
170 public void onPasswordRevoveryEvent(UserAccountEvent event
) throws MalformedURLException
, ExecutionException
, MailException
, AddressException
, AccountSelfManagementException
{
172 if(event
.getAction().equals(UserAccountEvent
.UserAccountAction
.REQUEST_PASSWORD_RESET
)) {
173 requestPasswordReset();
174 } else if(event
.getAction().equals(UserAccountEvent
.UserAccountAction
.REGISTER_ACCOUNT
)) {
175 requestAccountCreation();
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(
189 servletBaseUrl
.toString() + "/app/" + UserAccountSelfManagementUI
.NAME
+ "#!" + PasswordResetViewBean
.NAME
+ "/%s");
190 futureResult
.addCallback(
192 finshedSignal
.countDown();
195 // possible MailException
196 asyncException
.add(exception
);
197 finshedSignal
.countDown();
200 boolean asyncTimeout
= false;
201 Boolean result
= false;
203 finshedSignal
.await(2, TimeUnit
.SECONDS
);
204 result
= futureResult
.get();
205 } catch (InterruptedException e
) {
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
);
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>";
222 messageText
= "Sending the password reset email to you has failed. Please try again later or contact " + supportText
+ " in case this error persists.";
224 if(asyncException
.get(0) instanceof EmailAddressNotFoundException
) {
225 messageText
= "There is no user accout for this email address.";
227 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue(messageText
);
228 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme
.LABEL_FAILURE
);
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);
238 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue("A timeout has occured, please try again.");
239 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme
.LABEL_FAILURE
);
244 private void requestAccountCreation() throws MalformedURLException
, MailException
, AddressException
, AccountSelfManagementException
, ExecutionException
{
245 String emailAddress
= getView().getLoginDialog().getEmail().getValue();
246 URL servletBaseUrl
= VaadinServletUtilities
.getServletBaseUrl();
248 logger
.debug("UserAccountAction.REGISTER_ACCOUNT for " + servletBaseUrl
+ ", emailAddress:" + emailAddress
);
250 CountDownLatch finshedSignal
= new CountDownLatch(1);
251 List
<Throwable
> asyncExceptions
= new ArrayList
<>(1);
252 String passwordRequestFormUrlTemplate
= servletBaseUrl
.toString() + "/app/" + UserAccountSelfManagementUI
.NAME
+ "#!" + AccountRegistrationViewBean
.NAME
+ "/%s";
253 ListenableFuture
<Boolean
> futureResult
= repo
.getAccountRegistrationService()
254 .emailAccountRegistrationRequest(emailAddress
, passwordRequestFormUrlTemplate
);
255 futureResult
.addCallback(
257 finshedSignal
.countDown();
260 // possible MailException
261 asyncExceptions
.add(exception
);
262 finshedSignal
.countDown();
265 boolean asyncTimeout
= false;
266 Boolean result
= false;
268 finshedSignal
.await(4, TimeUnit
.SECONDS
);
269 result
= futureResult
.get();
270 } catch (InterruptedException e
) {
272 } catch (Exception e
) {
273 // in case executing emailAccountRegistrationRequest() causes an exception faster
274 // than futureResult.addCallback( can be processed, the exception
275 // can not be caught asynchronously
276 // so we are adding all these exceptions here
277 asyncExceptions
.add(e
);
279 if(!asyncExceptions
.isEmpty()) {
280 getView().getLoginDialog().getRegisterMessageLabel()
281 .setValue("Sending the account registration email to you has failed. Please try again later or contact the support in case this error persists.");
282 getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme
.LABEL_FAILURE
);
283 asyncExceptions
.stream().forEach(e
->{e
.printStackTrace(); logger
.error("Error when sending mail: ", e
.getMessage());});
285 if(!asyncTimeout
&& result
) {
286 getView().getLoginDialog().getRegisterMessageLabel().setValue("An email with with further instructions has been sent to you.");
287 getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme
.LABEL_SUCCESS
);
288 getView().getLoginDialog().getEmail().setEnabled(false);
289 getView().getLoginDialog().getRegisterButton().setEnabled(false);
292 getView().getLoginDialog().getRegisterMessageLabel().setValue("A timeout has occured, please try again.");
293 getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme
.LABEL_FAILURE
);