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
.lang3
.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
<LoginPresenter
,LoginView
>
74 implements EventBusListener
<AuthenticationAttemptEvent
> {
76 private static final long serialVersionUID
= 4020699735656994791L;
78 private static final Logger logger
= LogManager
.getLogger();
80 private final static String PROPNAME_USER
= "cdm-vaadin.login.usr";
82 private final static String PROPNAME_PASSWORD
= "cdm-vaadin.login.pwd";
84 private String redirectToState
;
86 protected EventBus
.UIEventBus uiEventBus
;
89 @Qualifier("cdmRepository")
90 private ICdmRepository repo
;
93 protected Environment env
;
96 // protected void eventViewBusSubscription(ViewEventBus viewEventBus) {
97 // viewEventBus.subscribe(this);
101 protected void setUIEventBus(EventBus
.UIEventBus uiEventBus
){
102 this.uiEventBus
= uiEventBus
;
103 uiEventBus
.subscribe(this);
106 public boolean authenticate(String userName
, String password
) {
108 getView().clearMessage();
110 UsernamePasswordAuthenticationToken token
= new UsernamePasswordAuthenticationToken(userName
, password
);
111 AuthenticationManager authenticationManager
= getRepo().getAuthenticationManager();
113 Authentication authentication
= authenticationManager
.authenticate(token
);
114 if(authentication
!= null && authentication
.isAuthenticated()) {
115 logger
.debug("user '" + userName
+ "' authenticated");
116 currentSecurityContext().setAuthentication(authentication
);
117 if(NavigationManager
.class.isAssignableFrom(getNavigationManager().getClass())){
118 uiEventBus
.publish(this, new AuthenticationSuccessEvent(userName
));
119 logger
.debug("redirecting to " + redirectToState
);
120 uiEventBus
.publish(this, new NavigationEvent(redirectToState
));
123 } catch (AuthenticationException e
){
124 getView().showErrorMessage("Login failed! Please check your username and password.");
130 public void handleViewEntered() {
132 List
<String
> redirectToStateTokens
= getNavigationManager().getCurrentViewParameters();
133 String currentViewName
= getNavigationManager().getCurrentViewName();
135 if(currentViewName
.equals(LoginViewBean
.NAME
) && redirectToStateTokens
.isEmpty()){
136 // login view is shown in turn to an explicit login request of the user (e.g. login button pressed)
137 // use the redirectToStateTokens 1-n as redirectToState
138 //FIXME implement : redirectToState = UserView.NAME
141 // the login view is shown instead of the requested view for which the user needs to login
142 redirectToState
= String
.join("/", redirectToStateTokens
);
145 getView().getLoginDialog().getEmail().addValidator(new AbstractStringValidator("An account for this email address already exits. You may want to use the \"Password Revovery\" tab intsead?") {
146 private static final long serialVersionUID
= 1L;
148 protected boolean isValidValue(String value
) {
149 return !repo
.getAccountRegistrationService().emailAddressExists(value
);
153 // attempt to auto login
154 if(StringUtils
.isNotEmpty(System
.getProperty(PROPNAME_USER
)) && StringUtils
.isNotEmpty(System
.getProperty(PROPNAME_PASSWORD
))){
155 logger
.warn("Performing autologin with user " + System
.getProperty(PROPNAME_USER
));
156 authenticate(System
.getProperty(PROPNAME_USER
), System
.getProperty(PROPNAME_PASSWORD
));
162 public void onEvent(Event
<AuthenticationAttemptEvent
> event
) {
163 if(getView()!= null){
164 authenticate(event
.getPayload().getUserName(), getView().getLoginDialog().getPassword().getValue());
166 logger
.info("view is NULL, not yet disposed LoginPresenter?");
170 @EventBusListenerMethod
171 public void onPasswordRevoveryEvent(UserAccountEvent event
) throws MalformedURLException
, ExecutionException
, MailException
, AddressException
, AccountSelfManagementException
{
173 if(event
.getAction().equals(UserAccountEvent
.UserAccountAction
.REQUEST_PASSWORD_RESET
)) {
174 requestPasswordReset();
175 } else if(event
.getAction().equals(UserAccountEvent
.UserAccountAction
.REGISTER_ACCOUNT
)) {
176 requestAccountCreation();
180 private void requestPasswordReset() throws MalformedURLException
{
181 String userNameOrEmail
= getView().getLoginDialog().getUserNameOrEmail().getValue();
182 URL servletBaseUrl
= VaadinServletUtilities
.getServletBaseUrl();
183 logger
.debug("UserAccountAction.REQUEST_PASSWORD_RESET for " + servletBaseUrl
+ ", userNameOrEmail:" + userNameOrEmail
);
184 // Implementation note: UI modifications allied in the below callback methods will not affect the UI
185 // immediately, therefore we use a CountDownLatch
186 CountDownLatch finshedSignal
= new CountDownLatch(1);
187 List
<Throwable
> asyncException
= new ArrayList
<>(1);
188 ListenableFuture
<Boolean
> futureResult
= repo
.getPasswordResetService().emailResetToken(
190 servletBaseUrl
.toString() + "/app/" + UserAccountSelfManagementUI
.NAME
+ "#!" + PasswordResetViewBean
.NAME
+ "/%s");
191 futureResult
.addCallback(
193 finshedSignal
.countDown();
196 // possible MailException
197 asyncException
.add(exception
);
198 finshedSignal
.countDown();
201 boolean asyncTimeout
= false;
202 Boolean result
= false;
204 finshedSignal
.await(2, TimeUnit
.SECONDS
);
205 result
= futureResult
.get();
206 } catch (InterruptedException e
) {
208 } catch (Exception e
) {
209 // in case executing getUserNameOrEmail() causes an exception faster
210 // than futureResult.addCallback( can be processed, the exception
211 // can not be caught asynchronously
212 // so we are adding all these exceptions here
213 asyncException
.add(e
);
215 if(!asyncException
.isEmpty()) {
216 String messageText
= "An unknown error has occurred.";
217 if(asyncException
.get(0) instanceof MailException
) {
218 String supportText
= "the support";
219 String supportEmail
= env
.getProperty(CdmConfigurationKeys
.MAIL_ADDRESS_SUPPORT
);
220 if(supportEmail
!= null) {
221 supportText
= "<a href=\"mailto:" + supportEmail
+"\">" + supportEmail
+ "</a>";
223 messageText
= "Sending the password reset email to you has failed. Please try again later or contact " + supportText
+ " in case this error persists.";
225 if(asyncException
.get(0) instanceof EmailAddressNotFoundException
) {
226 messageText
= "There is no user accout for this email address.";
228 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue(messageText
);
229 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme
.LABEL_FAILURE
);
231 if(!asyncTimeout
&& result
) {
232 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue("An email with a password reset link has been sent to you.");
233 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme
.LABEL_SUCCESS
);
234 getView().getLoginDialog().getSendOnetimeLogin().setEnabled(false);
235 getView().getLoginDialog().getUserNameOrEmail().setEnabled(false);
236 getView().getLoginDialog().getUserNameOrEmail().setReadOnly(true);
239 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue("A timeout has occured, please try again.");
240 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme
.LABEL_FAILURE
);
245 private void requestAccountCreation() throws MalformedURLException
, MailException
, AddressException
, AccountSelfManagementException
{
246 String emailAddress
= getView().getLoginDialog().getEmail().getValue();
247 URL servletBaseUrl
= VaadinServletUtilities
.getServletBaseUrl();
249 if (logger
.isDebugEnabled()) {logger
.debug("UserAccountAction.REGISTER_ACCOUNT for " + servletBaseUrl
+ ", emailAddress:" + emailAddress
);}
251 CountDownLatch finshedSignal
= new CountDownLatch(1);
252 List
<Throwable
> asyncExceptions
= new ArrayList
<>(1);
253 String passwordRequestFormUrlTemplate
= servletBaseUrl
.toString() + "/app/" + UserAccountSelfManagementUI
.NAME
+ "#!" + AccountRegistrationViewBean
.NAME
+ "/%s";
254 ListenableFuture
<Boolean
> futureResult
= repo
.getAccountRegistrationService()
255 .emailAccountRegistrationRequest(emailAddress
, passwordRequestFormUrlTemplate
);
256 futureResult
.addCallback(
258 finshedSignal
.countDown();
261 // possible MailException
262 asyncExceptions
.add(exception
);
263 finshedSignal
.countDown();
266 boolean asyncTimeout
= false;
267 Boolean result
= false;
269 finshedSignal
.await(4, TimeUnit
.SECONDS
);
270 result
= futureResult
.get();
271 } catch (InterruptedException e
) {
273 } catch (Exception e
) {
274 // in case executing emailAccountRegistrationRequest() causes an exception faster
275 // than futureResult.addCallback( can be processed, the exception
276 // can not be caught asynchronously
277 // so we are adding all these exceptions here
278 asyncExceptions
.add(e
);
280 if(!asyncExceptions
.isEmpty()) {
281 getView().getLoginDialog().getRegisterMessageLabel()
282 .setValue("Sending the account registration email to you has failed. Please try again later or contact the support in case this error persists.");
283 getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme
.LABEL_FAILURE
);
284 asyncExceptions
.stream().forEach(e
->{e
.printStackTrace(); logger
.error("Error when sending mail: ", e
.getMessage());});
286 if(!asyncTimeout
&& result
) {
287 getView().getLoginDialog().getRegisterMessageLabel().setValue("An email with with further instructions has been sent to you.");
288 getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme
.LABEL_SUCCESS
);
289 getView().getLoginDialog().getEmail().setEnabled(false);
290 getView().getLoginDialog().getRegisterButton().setEnabled(false);
293 getView().getLoginDialog().getRegisterMessageLabel().setValue("A timeout has occured, please try again.");
294 getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme
.LABEL_FAILURE
);