cleanup
[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.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;
37
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;
42
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;
55
56 /**
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.
60 * <p>
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.:
64 * <pre>
65 * -Dcdm-vaadin.login.usr=admin -Dcdm-vaadin.login.pwd=00000
66 * </pre>
67 *
68 * @author a.kohlbecker
69 * @since Apr 25, 2017
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 logger = LogManager.getLogger();
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 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));
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 logger.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 logger.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> 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(
256 successFuture -> {
257 finshedSignal.countDown();
258 },
259 exception -> {
260 // possible MailException
261 asyncExceptions.add(exception);
262 finshedSignal.countDown();
263 }
264 );
265 boolean asyncTimeout = false;
266 Boolean result = false;
267 try {
268 finshedSignal.await(4, TimeUnit.SECONDS);
269 result = futureResult.get();
270 } catch (InterruptedException e) {
271 asyncTimeout = true;
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);
278 }
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());});
284 } else {
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);
290
291 } else {
292 getView().getLoginDialog().getRegisterMessageLabel().setValue("A timeout has occured, please try again.");
293 getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme.LABEL_FAILURE);
294 }
295 }
296 }
297 }