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
.log4j
.Logger
;
23 import org
.springframework
.beans
.factory
.annotation
.Autowired
;
24 import org
.springframework
.beans
.factory
.annotation
.Qualifier
;
25 import org
.springframework
.mail
.MailException
;
26 import org
.springframework
.security
.authentication
.AuthenticationManager
;
27 import org
.springframework
.security
.authentication
.UsernamePasswordAuthenticationToken
;
28 import org
.springframework
.security
.core
.Authentication
;
29 import org
.springframework
.security
.core
.AuthenticationException
;
30 import org
.springframework
.util
.concurrent
.ListenableFuture
;
31 import org
.vaadin
.spring
.events
.Event
;
32 import org
.vaadin
.spring
.events
.EventBus
;
33 import org
.vaadin
.spring
.events
.EventBusListener
;
34 import org
.vaadin
.spring
.events
.annotation
.EventBusListenerMethod
;
36 import com
.vaadin
.data
.validator
.AbstractStringValidator
;
37 import com
.vaadin
.spring
.annotation
.SpringComponent
;
38 import com
.vaadin
.spring
.annotation
.ViewScope
;
39 import com
.vaadin
.ui
.themes
.ValoTheme
;
41 import eu
.etaxonomy
.cdm
.api
.application
.ICdmRepository
;
42 import eu
.etaxonomy
.cdm
.api
.service
.security
.AccountSelfManagementException
;
43 import eu
.etaxonomy
.cdm
.api
.service
.security
.EmailAddressNotFoundException
;
44 import eu
.etaxonomy
.cdm
.vaadin
.event
.AuthenticationAttemptEvent
;
45 import eu
.etaxonomy
.cdm
.vaadin
.event
.AuthenticationSuccessEvent
;
46 import eu
.etaxonomy
.cdm
.vaadin
.event
.UserAccountEvent
;
47 import eu
.etaxonomy
.cdm
.vaadin
.ui
.UserAccountSelfManagementUI
;
48 import eu
.etaxonomy
.cdm
.vaadin
.util
.VaadinServletUtilities
;
49 import eu
.etaxonomy
.vaadin
.mvp
.AbstractPresenter
;
50 import eu
.etaxonomy
.vaadin
.ui
.navigation
.NavigationEvent
;
51 import eu
.etaxonomy
.vaadin
.ui
.navigation
.NavigationManager
;
54 * The {@link LoginView} is used as replacement view in the scope of other views.
55 * Therefore the LoginPresenter must be in <b>UIScope</b> so that the LoginPresenter
56 * is available to all Views.
58 * The LoginPresenter offers a <b>auto login feature for developers</b>. To activate the auto login
59 * you need to provide the <code>user name</code> and <code>password</code> using the environment variables
60 * <code>cdm-vaadin.login.usr</code> and <code>cdm-vaadin.login.pwd</code>, e.g.:
62 * -Dcdm-vaadin.login.usr=admin -Dcdm-vaadin.login.pwd=00000
65 * @author a.kohlbecker
71 public class LoginPresenter
extends AbstractPresenter
<LoginView
> implements EventBusListener
<AuthenticationAttemptEvent
> {
73 private static final long serialVersionUID
= 4020699735656994791L;
75 private static final Logger log
= Logger
.getLogger(LoginPresenter
.class);
77 private final static String PROPNAME_USER
= "cdm-vaadin.login.usr";
79 private final static String PROPNAME_PASSWORD
= "cdm-vaadin.login.pwd";
81 private String redirectToState
;
83 protected EventBus
.UIEventBus uiEventBus
;
86 @Qualifier("cdmRepository")
87 private ICdmRepository repo
;
90 // protected void eventViewBusSubscription(ViewEventBus viewEventBus) {
91 // viewEventBus.subscribe(this);
95 protected void setUIEventBus(EventBus
.UIEventBus uiEventBus
){
96 this.uiEventBus
= uiEventBus
;
97 uiEventBus
.subscribe(this);
100 public boolean authenticate(String userName
, String password
) {
102 getView().clearMessage();
104 UsernamePasswordAuthenticationToken token
= new UsernamePasswordAuthenticationToken(userName
, password
);
105 AuthenticationManager authenticationManager
= getRepo().getAuthenticationManager();
107 Authentication authentication
= authenticationManager
.authenticate(token
);
108 if(authentication
!= null && authentication
.isAuthenticated()) {
109 log
.debug("user '" + userName
+ "' authenticated");
110 currentSecurityContext().setAuthentication(authentication
);
111 if(NavigationManager
.class.isAssignableFrom(getNavigationManager().getClass())){
112 uiEventBus
.publish(this, new AuthenticationSuccessEvent(userName
));
113 log
.debug("redirecting to " + redirectToState
);
114 uiEventBus
.publish(this, new NavigationEvent(redirectToState
));
117 } catch (AuthenticationException e
){
118 getView().showErrorMessage("Login failed! Please check your username and password.");
124 public void handleViewEntered() {
126 List
<String
> redirectToStateTokens
= getNavigationManager().getCurrentViewParameters();
127 String currentViewName
= getNavigationManager().getCurrentViewName();
129 if(currentViewName
.equals(LoginViewBean
.NAME
) && redirectToStateTokens
.isEmpty()){
130 // login view is shown in turn to an explicit login request of the user (e.g. login button pressed)
131 // use the redirectToStateTokens 1-n as redirectToState
132 //FIXME implement : redirectToState = UserView.NAME
135 // the login view is shown instead of the requested view for which the user needs to login
136 redirectToState
= String
.join("/", redirectToStateTokens
);
139 getView().getLoginDialog().getEmail().addValidator(new AbstractStringValidator("An account for this email address already exits. You may want to use the \"Password Revovery\" tab intsead?") {
140 private static final long serialVersionUID
= 1L;
142 protected boolean isValidValue(String value
) {
143 return !repo
.getAccountRegistrationService().emailAddressExists(value
);
147 // attempt to auto login
148 if(StringUtils
.isNotEmpty(System
.getProperty(PROPNAME_USER
)) && StringUtils
.isNotEmpty(System
.getProperty(PROPNAME_PASSWORD
))){
149 log
.warn("Performing autologin with user " + System
.getProperty(PROPNAME_USER
));
150 authenticate(System
.getProperty(PROPNAME_USER
), System
.getProperty(PROPNAME_PASSWORD
));
156 public void onEvent(Event
<AuthenticationAttemptEvent
> event
) {
157 if(getView()!= null){
158 authenticate(event
.getPayload().getUserName(), getView().getLoginDialog().getPassword().getValue());
160 log
.info("view is NULL, not yet disposed LoginPresenter?");
164 @EventBusListenerMethod
165 public void onPasswordRevoveryEvent(UserAccountEvent event
) throws MalformedURLException
, ExecutionException
, MailException
, AddressException
, AccountSelfManagementException
{
167 if(event
.getAction().equals(UserAccountEvent
.UserAccountAction
.REQUEST_PASSWORD_RESET
)) {
168 requestPasswordReset();
169 } else if(event
.getAction().equals(UserAccountEvent
.UserAccountAction
.REGISTER_ACCOUNT
)) {
170 requestAccountCreation();
174 private void requestPasswordReset() throws MalformedURLException
, ExecutionException
{
175 String userNameOrEmail
= getView().getLoginDialog().getUserNameOrEmail().getValue();
176 URL servletBaseUrl
= VaadinServletUtilities
.getServletBaseUrl();
177 logger
.debug("UserAccountAction.REQUEST_PASSWORD_RESET for " + servletBaseUrl
+ ", userNameOrEmail:" + userNameOrEmail
);
178 // Implementation note: UI modifications allied in the below callback methods will not affect the UI
179 // immediately, therefore we use a CountDownLatch
180 CountDownLatch finshedSignal
= new CountDownLatch(1);
181 List
<Throwable
> asyncException
= new ArrayList
<>(1);
182 ListenableFuture
<Boolean
> futureResult
= repo
.getPasswordResetService().emailResetToken(
184 servletBaseUrl
.toString() + "/app/" + UserAccountSelfManagementUI
.NAME
+ "#!" + PasswordResetViewBean
.NAME
+ "/%s");
185 futureResult
.addCallback(
187 finshedSignal
.countDown();
190 // possible MailException
191 asyncException
.add(exception
);
192 finshedSignal
.countDown();
195 boolean asyncTimeout
= false;
196 Boolean result
= false;
198 finshedSignal
.await(2, TimeUnit
.SECONDS
);
199 result
= futureResult
.get();
200 } catch (InterruptedException e
) {
202 } catch (Exception e
) {
203 // in case executing getUserNameOrEmail() causes an exeption faster
204 // than futureResult.addCallback( can be processed, the execption
205 // can not be caught asynchronously
206 // so we are adding all these exceptions here
207 asyncException
.add(e
);
209 if(!asyncException
.isEmpty()) {
210 String messageText
= "An unknown error has occurred.";
211 if(asyncException
.get(0) instanceof MailException
) {
212 messageText
= "Sending the password reset email to you has failed. Please try again later or contect the support in case this error persists.";
214 if(asyncException
.get(0) instanceof EmailAddressNotFoundException
) {
215 messageText
= "There is no user accout for this email address.";
217 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue(messageText
);
218 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme
.LABEL_FAILURE
);
220 if(!asyncTimeout
&& result
) {
221 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue("An email with a password reset link has been sent to you.");
222 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme
.LABEL_SUCCESS
);
223 getView().getLoginDialog().getSendOnetimeLogin().setEnabled(false);
224 getView().getLoginDialog().getUserNameOrEmail().setEnabled(false);
225 getView().getLoginDialog().getUserNameOrEmail().setReadOnly(true);
228 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue("A timeout has occured, please try again.");
229 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme
.LABEL_FAILURE
);
234 private void requestAccountCreation() throws MalformedURLException
, MailException
, AddressException
, AccountSelfManagementException
, ExecutionException
{
235 String emailAddress
= getView().getLoginDialog().getEmail().getValue();
236 URL servletBaseUrl
= VaadinServletUtilities
.getServletBaseUrl();
238 logger
.debug("UserAccountAction.REGISTER_ACCOUNT for " + servletBaseUrl
+ ", emailAddress:" + emailAddress
);
240 CountDownLatch finshedSignal
= new CountDownLatch(1);
241 List
<Throwable
> asyncException
= new ArrayList
<>(1);
242 ListenableFuture
<Boolean
> futureResult
= repo
.getAccountRegistrationService().emailAccountRegistrationRequest(emailAddress
,
243 servletBaseUrl
.toString() + "/app/" + UserAccountSelfManagementUI
.NAME
+ "#!" + AccountRegistrationViewBean
.NAME
+ "/%s");
244 futureResult
.addCallback(
246 finshedSignal
.countDown();
249 // possible MailException
250 asyncException
.add(exception
);
251 finshedSignal
.countDown();
254 boolean asyncTimeout
= false;
255 Boolean result
= false;
257 finshedSignal
.await(2, TimeUnit
.SECONDS
);
258 result
= futureResult
.get();
259 } catch (InterruptedException e
) {
262 if(!asyncException
.isEmpty()) {
263 getView().getLoginDialog().getRegisterMessageLabel()
264 .setValue("Sending the account resitration email to you has failed. Please try again later or contect the support in case this error persists.");
265 getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme
.LABEL_FAILURE
);
267 if(!asyncTimeout
&& result
) {
268 getView().getLoginDialog().getRegisterMessageLabel().setValue("An email with with further instructions has been sent to you.");
269 getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme
.LABEL_SUCCESS
);
270 getView().getLoginDialog().getEmail().setEnabled(false);
271 getView().getLoginDialog().getRegisterButton().setEnabled(false);
274 getView().getLoginDialog().getRegisterMessageLabel().setValue("A timeout has occured, please try again.");
275 getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme
.LABEL_FAILURE
);