d49975427390549e3a80629725b918ff254d22ec
[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.core.env.Environment;
26 import org.springframework.mail.MailException;
27 import org.springframework.security.authentication.AuthenticationManager;
28 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
29 import org.springframework.security.core.Authentication;
30 import org.springframework.security.core.AuthenticationException;
31 import org.springframework.util.concurrent.ListenableFuture;
32 import org.vaadin.spring.events.Event;
33 import org.vaadin.spring.events.EventBus;
34 import org.vaadin.spring.events.EventBusListener;
35 import org.vaadin.spring.events.annotation.EventBusListenerMethod;
36
37 import com.vaadin.data.validator.AbstractStringValidator;
38 import com.vaadin.spring.annotation.SpringComponent;
39 import com.vaadin.spring.annotation.ViewScope;
40 import com.vaadin.ui.themes.ValoTheme;
41
42 import eu.etaxonomy.cdm.api.application.ICdmRepository;
43 import eu.etaxonomy.cdm.api.config.CdmConfigurationKeys;
44 import eu.etaxonomy.cdm.api.service.security.AccountSelfManagementException;
45 import eu.etaxonomy.cdm.api.service.security.EmailAddressNotFoundException;
46 import eu.etaxonomy.cdm.vaadin.event.AuthenticationAttemptEvent;
47 import eu.etaxonomy.cdm.vaadin.event.AuthenticationSuccessEvent;
48 import eu.etaxonomy.cdm.vaadin.event.UserAccountEvent;
49 import eu.etaxonomy.cdm.vaadin.ui.UserAccountSelfManagementUI;
50 import eu.etaxonomy.cdm.vaadin.util.VaadinServletUtilities;
51 import eu.etaxonomy.vaadin.mvp.AbstractPresenter;
52 import eu.etaxonomy.vaadin.ui.navigation.NavigationEvent;
53 import eu.etaxonomy.vaadin.ui.navigation.NavigationManager;
54
55 /**
56 * The {@link LoginView} is used as replacement view in the scope of other views.
57 * Therefore the LoginPresenter must be in <b>UIScope</b> so that the LoginPresenter
58 * is available to all Views.
59 * <p>
60 * The LoginPresenter offers a <b>auto login feature for developers</b>. To activate the auto login
61 * you need to provide the <code>user name</code> and <code>password</code> using the environment variables
62 * <code>cdm-vaadin.login.usr</code> and <code>cdm-vaadin.login.pwd</code>, e.g.:
63 * <pre>
64 * -Dcdm-vaadin.login.usr=admin -Dcdm-vaadin.login.pwd=00000
65 * </pre>
66 *
67 * @author a.kohlbecker
68 * @since Apr 25, 2017
69 *
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 log = Logger.getLogger(LoginPresenter.class);
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 log.debug("user '" + userName + "' authenticated");
115 currentSecurityContext().setAuthentication(authentication);
116 if(NavigationManager.class.isAssignableFrom(getNavigationManager().getClass())){
117 uiEventBus.publish(this, new AuthenticationSuccessEvent(userName));
118 log.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 log.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 log.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 exeption faster
209 // than futureResult.addCallback( can be processed, the execption
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> asyncException = new ArrayList<>(1);
252 ListenableFuture<Boolean> futureResult = repo.getAccountRegistrationService().emailAccountRegistrationRequest(emailAddress,
253 servletBaseUrl.toString() + "/app/" + UserAccountSelfManagementUI.NAME + "#!" + AccountRegistrationViewBean.NAME + "/%s");
254 futureResult.addCallback(
255 successFuture -> {
256 finshedSignal.countDown();
257 },
258 exception -> {
259 // possible MailException
260 asyncException.add(exception);
261 finshedSignal.countDown();
262 }
263 );
264 boolean asyncTimeout = false;
265 Boolean result = false;
266 try {
267 finshedSignal.await(2, TimeUnit.SECONDS);
268 result = futureResult.get();
269 } catch (InterruptedException e) {
270 asyncTimeout = true;
271 }
272 if(!asyncException.isEmpty()) {
273 getView().getLoginDialog().getRegisterMessageLabel()
274 .setValue("Sending the account resitration email to you has failed. Please try again later or contect the support in case this error persists.");
275 getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme.LABEL_FAILURE);
276 } else {
277 if(!asyncTimeout && result) {
278 getView().getLoginDialog().getRegisterMessageLabel().setValue("An email with with further instructions has been sent to you.");
279 getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme.LABEL_SUCCESS);
280 getView().getLoginDialog().getEmail().setEnabled(false);
281 getView().getLoginDialog().getRegisterButton().setEnabled(false);
282
283 } else {
284 getView().getLoginDialog().getRegisterMessageLabel().setValue("A timeout has occured, please try again.");
285 getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme.LABEL_FAILURE);
286 }
287 }
288 }
289
290 }