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
|
}
|