ref #9498 providing feedback to users in case an unknown email has been entered
[cdm-vaadin.git] / src / main / java / eu / etaxonomy / cdm / vaadin / view / LoginPresenter.java
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.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;
35
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;
40
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;
52
53 /**
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.
57 * <p>
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.:
61 * <pre>
62 * -Dcdm-vaadin.login.usr=admin -Dcdm-vaadin.login.pwd=00000
63 * </pre>
64 *
65 * @author a.kohlbecker
66 * @since Apr 25, 2017
67 *
68 */
69 @SpringComponent
70 @ViewScope
71 public class LoginPresenter extends AbstractPresenter<LoginView> implements EventBusListener<AuthenticationAttemptEvent> {
72
73 private static final long serialVersionUID = 4020699735656994791L;
74
75 private static final Logger log = Logger.getLogger(LoginPresenter.class);
76
77 private final static String PROPNAME_USER = "cdm-vaadin.login.usr";
78
79 private final static String PROPNAME_PASSWORD = "cdm-vaadin.login.pwd";
80
81 private String redirectToState;
82
83 protected EventBus.UIEventBus uiEventBus;
84
85 @Autowired
86 @Qualifier("cdmRepository")
87 private ICdmRepository repo;
88
89 // @Override
90 // protected void eventViewBusSubscription(ViewEventBus viewEventBus) {
91 // viewEventBus.subscribe(this);
92 // }
93
94 @Autowired
95 protected void setUIEventBus(EventBus.UIEventBus uiEventBus){
96 this.uiEventBus = uiEventBus;
97 uiEventBus.subscribe(this);
98 }
99
100 public boolean authenticate(String userName, String password) {
101
102 getView().clearMessage();
103
104 UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userName, password);
105 AuthenticationManager authenticationManager = getRepo().getAuthenticationManager();
106 try {
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));
115 }
116 }
117 } catch (AuthenticationException e){
118 getView().showErrorMessage("Login failed! Please check your username and password.");
119 }
120 return false;
121 }
122
123 @Override
124 public void handleViewEntered() {
125
126 List<String> redirectToStateTokens = getNavigationManager().getCurrentViewParameters();
127 String currentViewName = getNavigationManager().getCurrentViewName();
128
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
133
134 } else {
135 // the login view is shown instead of the requested view for which the user needs to login
136 redirectToState = String.join("/", redirectToStateTokens);
137 }
138
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;
141 @Override
142 protected boolean isValidValue(String value) {
143 return !repo.getAccountRegistrationService().emailAddressExists(value);
144 }
145 });
146
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));
151 }
152
153 }
154
155 @Override
156 public void onEvent(Event<AuthenticationAttemptEvent> event) {
157 if(getView()!= null){
158 authenticate(event.getPayload().getUserName(), getView().getLoginDialog().getPassword().getValue());
159 } else {
160 log.info("view is NULL, not yet disposed LoginPresenter?");
161 }
162 }
163
164 @EventBusListenerMethod
165 public void onPasswordRevoveryEvent(UserAccountEvent event) throws MalformedURLException, ExecutionException, MailException, AddressException, AccountSelfManagementException {
166
167 if(event.getAction().equals(UserAccountEvent.UserAccountAction.REQUEST_PASSWORD_RESET)) {
168 requestPasswordReset();
169 } else if(event.getAction().equals(UserAccountEvent.UserAccountAction.REGISTER_ACCOUNT)) {
170 requestAccountCreation();
171 }
172 }
173
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(
183 userNameOrEmail,
184 servletBaseUrl.toString() + "/app/" + UserAccountSelfManagementUI.NAME + "#!" + PasswordResetViewBean.NAME + "/%s");
185 futureResult.addCallback(
186 successFuture -> {
187 finshedSignal.countDown();
188 },
189 exception -> {
190 // possible MailException
191 asyncException.add(exception);
192 finshedSignal.countDown();
193 }
194 );
195 boolean asyncTimeout = false;
196 Boolean result = false;
197 try {
198 finshedSignal.await(2, TimeUnit.SECONDS);
199 result = futureResult.get();
200 } catch (InterruptedException e) {
201 asyncTimeout = true;
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);
208 }
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.";
213 }
214 if(asyncException.get(0) instanceof EmailAddressNotFoundException) {
215 messageText = "There is no user accout for this email address.";
216 }
217 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue(messageText);
218 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme.LABEL_FAILURE);
219 } else {
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);
226
227 } else {
228 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue("A timeout has occured, please try again.");
229 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme.LABEL_FAILURE);
230 }
231 }
232 }
233
234 private void requestAccountCreation() throws MalformedURLException, MailException, AddressException, AccountSelfManagementException, ExecutionException {
235 String emailAddress = getView().getLoginDialog().getEmail().getValue();
236 URL servletBaseUrl = VaadinServletUtilities.getServletBaseUrl();
237
238 logger.debug("UserAccountAction.REGISTER_ACCOUNT for " + servletBaseUrl + ", emailAddress:" + emailAddress);
239
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(
245 successFuture -> {
246 finshedSignal.countDown();
247 },
248 exception -> {
249 // possible MailException
250 asyncException.add(exception);
251 finshedSignal.countDown();
252 }
253 );
254 boolean asyncTimeout = false;
255 Boolean result = false;
256 try {
257 finshedSignal.await(2, TimeUnit.SECONDS);
258 result = futureResult.get();
259 } catch (InterruptedException e) {
260 asyncTimeout = true;
261 }
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);
266 } else {
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);
272
273 } else {
274 getView().getLoginDialog().getRegisterMessageLabel().setValue("A timeout has occured, please try again.");
275 getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme.LABEL_FAILURE);
276 }
277 }
278 }
279
280 }