ref #10302 change order in NomenclaturalStatusRow
[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.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;
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<LoginPresenter,LoginView>
74 implements EventBusListener<AuthenticationAttemptEvent> {
75
76 private static final long serialVersionUID = 4020699735656994791L;
77
78 private static final Logger logger = LogManager.getLogger();
79
80 private final static String PROPNAME_USER = "cdm-vaadin.login.usr";
81
82 private final static String PROPNAME_PASSWORD = "cdm-vaadin.login.pwd";
83
84 private String redirectToState;
85
86 protected EventBus.UIEventBus uiEventBus;
87
88 @Autowired
89 @Qualifier("cdmRepository")
90 private ICdmRepository repo;
91
92 @Autowired
93 protected Environment env;
94
95 // @Override
96 // protected void eventViewBusSubscription(ViewEventBus viewEventBus) {
97 // viewEventBus.subscribe(this);
98 // }
99
100 @Autowired
101 protected void setUIEventBus(EventBus.UIEventBus uiEventBus){
102 this.uiEventBus = uiEventBus;
103 uiEventBus.subscribe(this);
104 }
105
106 public boolean authenticate(String userName, String password) {
107
108 getView().clearMessage();
109
110 UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userName, password);
111 AuthenticationManager authenticationManager = getRepo().getAuthenticationManager();
112 try {
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));
121 }
122 }
123 } catch (AuthenticationException e){
124 getView().showErrorMessage("Login failed! Please check your username and password.");
125 }
126 return false;
127 }
128
129 @Override
130 public void handleViewEntered() {
131
132 List<String> redirectToStateTokens = getNavigationManager().getCurrentViewParameters();
133 String currentViewName = getNavigationManager().getCurrentViewName();
134
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
139
140 } else {
141 // the login view is shown instead of the requested view for which the user needs to login
142 redirectToState = String.join("/", redirectToStateTokens);
143 }
144
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;
147 @Override
148 protected boolean isValidValue(String value) {
149 return !repo.getAccountRegistrationService().emailAddressExists(value);
150 }
151 });
152
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));
157 }
158
159 }
160
161 @Override
162 public void onEvent(Event<AuthenticationAttemptEvent> event) {
163 if(getView()!= null){
164 authenticate(event.getPayload().getUserName(), getView().getLoginDialog().getPassword().getValue());
165 } else {
166 logger.info("view is NULL, not yet disposed LoginPresenter?");
167 }
168 }
169
170 @EventBusListenerMethod
171 public void onPasswordRevoveryEvent(UserAccountEvent event) throws MalformedURLException, ExecutionException, MailException, AddressException, AccountSelfManagementException {
172
173 if(event.getAction().equals(UserAccountEvent.UserAccountAction.REQUEST_PASSWORD_RESET)) {
174 requestPasswordReset();
175 } else if(event.getAction().equals(UserAccountEvent.UserAccountAction.REGISTER_ACCOUNT)) {
176 requestAccountCreation();
177 }
178 }
179
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(
189 userNameOrEmail,
190 servletBaseUrl.toString() + "/app/" + UserAccountSelfManagementUI.NAME + "#!" + PasswordResetViewBean.NAME + "/%s");
191 futureResult.addCallback(
192 successFuture -> {
193 finshedSignal.countDown();
194 },
195 exception -> {
196 // possible MailException
197 asyncException.add(exception);
198 finshedSignal.countDown();
199 }
200 );
201 boolean asyncTimeout = false;
202 Boolean result = false;
203 try {
204 finshedSignal.await(2, TimeUnit.SECONDS);
205 result = futureResult.get();
206 } catch (InterruptedException e) {
207 asyncTimeout = true;
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);
214 }
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>";
222 }
223 messageText = "Sending the password reset email to you has failed. Please try again later or contact " + supportText + " in case this error persists.";
224 }
225 if(asyncException.get(0) instanceof EmailAddressNotFoundException) {
226 messageText = "There is no user accout for this email address.";
227 }
228 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue(messageText);
229 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme.LABEL_FAILURE);
230 } else {
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);
237
238 } else {
239 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue("A timeout has occured, please try again.");
240 getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme.LABEL_FAILURE);
241 }
242 }
243 }
244
245 private void requestAccountCreation() throws MalformedURLException, MailException, AddressException, AccountSelfManagementException {
246 String emailAddress = getView().getLoginDialog().getEmail().getValue();
247 URL servletBaseUrl = VaadinServletUtilities.getServletBaseUrl();
248
249 if (logger.isDebugEnabled()) {logger.debug("UserAccountAction.REGISTER_ACCOUNT for " + servletBaseUrl + ", emailAddress:" + emailAddress);}
250
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(
257 successFuture -> {
258 finshedSignal.countDown();
259 },
260 exception -> {
261 // possible MailException
262 asyncExceptions.add(exception);
263 finshedSignal.countDown();
264 }
265 );
266 boolean asyncTimeout = false;
267 Boolean result = false;
268 try {
269 finshedSignal.await(4, TimeUnit.SECONDS);
270 result = futureResult.get();
271 } catch (InterruptedException e) {
272 asyncTimeout = true;
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);
279 }
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());});
285 } else {
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);
291
292 } else {
293 getView().getLoginDialog().getRegisterMessageLabel().setValue("A timeout has occured, please try again.");
294 getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme.LABEL_FAILURE);
295 }
296 }
297 }
298 }