1 package net.sumaris.server.service.administration;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 import com.google.common.base.Preconditions;
26 import com.google.common.collect.ImmutableList;
27 import com.google.common.collect.Lists;
28 import it.ozimov.springboot.mail.model.Email;
29 import it.ozimov.springboot.mail.model.defaultimpl.DefaultEmail;
30 import it.ozimov.springboot.mail.service.EmailService;
31 import net.sumaris.core.dao.administration.user.PersonDao;
32 import net.sumaris.core.dao.administration.user.UserSettingsDao;
33 import net.sumaris.core.dao.administration.user.UserTokenDao;
34 import net.sumaris.core.exception.DataNotFoundException;
35 import net.sumaris.core.exception.SumarisTechnicalException;
36 import net.sumaris.core.model.administration.user.Person;
37 import net.sumaris.core.model.referential.StatusEnum;
38 import net.sumaris.core.model.referential.UserProfileEnum;
39 import net.sumaris.core.vo.administration.user.AccountVO;
40 import net.sumaris.core.vo.administration.user.PersonVO;
41 import net.sumaris.core.vo.administration.user.UserSettingsVO;
42 import net.sumaris.core.vo.filter.PersonFilterVO;
43 import net.sumaris.server.config.SumarisServerConfiguration;
44 import net.sumaris.server.config.SumarisServerConfigurationOption;
45 import net.sumaris.server.exception.ErrorCodes;
46 import net.sumaris.server.exception.InvalidEmailConfirmationException;
47 import net.sumaris.server.service.crypto.ServerCryptoService;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50 import org.nuiton.i18n.I18n;
51 import org.springframework.beans.BeanUtils;
52 import org.springframework.beans.factory.annotation.Autowired;
53 import org.springframework.core.convert.support.GenericConversionService;
54 import org.springframework.dao.DataRetrievalFailureException;
55 import org.springframework.stereotype.Service;
56 import org.apache.commons.collections.CollectionUtils;
57 import org.springframework.util.StringUtils;
58
59 import javax.annotation.PostConstruct;
60 import javax.mail.internet.AddressException;
61 import javax.mail.internet.InternetAddress;
62 import java.io.UnsupportedEncodingException;
63 import java.nio.charset.Charset;
64 import java.util.List;
65 import java.util.Locale;
66 import java.util.Objects;
67 import java.util.stream.Collectors;
68
69 @Service("accountService")
70 public class AccountServiceImpl implements AccountService {
71
72
73
74 private static final Logger log = LoggerFactory.getLogger(AccountServiceImpl.class);
75
76 private static final Charset CHARSET_UTF8 = Charset.forName("UTF-8");
77
78 @Autowired
79 private PersonDao personDao;
80
81 @Autowired
82 private UserSettingsDao userSettingsDao;
83
84 @Autowired
85 private UserTokenDao userTokenDao;
86
87 @Autowired
88 private EmailService emailService;
89
90 @Autowired
91 private ServerCryptoService serverCryptoService;
92
93 @Autowired
94 private GenericConversionService conversionService;
95
96 @Autowired
97 private AccountService self;
98
99 private SumarisServerConfiguration config;
100
101 private InternetAddress mailFromAddress;
102
103 private String serverUrl;
104
105 @Autowired
106 public AccountServiceImpl(SumarisServerConfiguration config) {
107 this.config = config;
108
109
110 String mailFrom = config.getMailFrom();
111 if (StringUtils.isEmpty(mailFrom)) {
112 mailFrom = config.getAdminMail();
113 }
114 if (StringUtils.isEmpty(mailFrom)) {
115 log.warn(I18n.t("sumaris.error.account.register.mail.disable", SumarisServerConfigurationOption.MAIL_FROM.name()));
116 this.mailFromAddress = null;
117 }
118 else {
119 try {
120 this.mailFromAddress = new InternetAddress(mailFrom, config.getAppName());
121 } catch (UnsupportedEncodingException e) {
122 throw new SumarisTechnicalException(I18n.t("sumaris.error.email.invalid", mailFrom, e.getMessage()), e);
123 }
124 }
125
126
127 this.serverUrl = config.getServerUrl();
128 }
129
130 @PostConstruct
131 public void registerConverter() {
132 log.debug("Register {Account} converters");
133 conversionService.addConverter(PersonVO.class, AccountVO.class, p -> self.toAccountVO(p));
134 conversionService.addConverter(Person.class, AccountVO.class, p -> self.getByPubkey(p.getPubkey()));
135 }
136
137 @Override
138 public AccountVO getByPubkey(String pubkey) {
139
140 PersonVO person = personDao.getByPubkeyOrNull(pubkey);
141 if (person == null) {
142 throw new DataRetrievalFailureException(I18n.t("sumaris.error.account.notFound"));
143 }
144
145 AccountVOation/user/AccountVO.html#AccountVO">AccountVO account = new AccountVO();
146 BeanUtils.copyProperties(person, account);
147
148 UserSettingsVO settings = userSettingsDao.getByIssuer(account.getPubkey());
149 account.setSettings(settings);
150
151 return account;
152 }
153
154 @Override
155 public AccountVO/../../../net/sumaris/core/vo/administration/user/AccountVO.html#AccountVO">AccountVO saveAccount(AccountVO account) {
156 if (account != null && account.getId() == null) {
157 return createAccount(account);
158 }
159
160 return updateAccount(account);
161 }
162
163 @Override
164 public AccountVO./../../net/sumaris/core/vo/administration/user/AccountVO.html#AccountVO">AccountVO createAccount(AccountVO account) {
165
166
167 checkValid(account);
168
169
170 PersonFilterVOFilterVO.html#PersonFilterVO">PersonFilterVO filter = new PersonFilterVO();
171 BeanUtils.copyProperties(account, filter);
172 List<PersonVO> duplicatedPersons = personDao.findByFilter(filter, 0, 2, null, null);
173 if (CollectionUtils.isNotEmpty(duplicatedPersons)) {
174 throw new SumarisTechnicalException(ErrorCodes.ACCOUNT_ALREADY_EXISTS, I18n.t("sumaris.error.account.register.duplicatedPerson"));
175 }
176
177
178
179
180
181 if (this.mailFromAddress == null) {
182 log.debug(I18n.t("sumaris.server.account.register.mail.skip"));
183 account.setStatusId(config.getStatusIdValid());
184 }
185 else {
186
187 account.setStatusId(config.getStatusIdTemporary());
188 }
189
190
191 account.setProfiles(Lists.newArrayList(UserProfileEnum.GUEST.label));
192
193
194 account.setEmail(org.apache.commons.lang3.StringUtils.trimToNull(account.getEmail()));
195
196
197 AccountVO../../net/sumaris/core/vo/administration/user/AccountVO.html#AccountVO">AccountVO savedAccount = (AccountVO) personDao.save(account);
198
199
200 if (account.getSettings() != null) {
201 account.getSettings().setIssuer(account.getPubkey());
202 UserSettingsVO savedSettings = userSettingsDao.save(account.getSettings());
203 savedAccount.setSettings(savedSettings);
204 }
205
206
207 sendConfirmationLinkByEmail(
208 account.getEmail(),
209 getLocale(account.getSettings().getLocale()));
210
211 return savedAccount;
212 }
213
214 @Override
215 public AccountVO./../../net/sumaris/core/vo/administration/user/AccountVO.html#AccountVO">AccountVO updateAccount(AccountVO account) {
216
217
218 checkValid(account);
219 Preconditions.checkNotNull(account.getId());
220
221
222 PersonVO existingPerson = personDao.get(account.getId());
223 if (existingPerson == null) {
224 throw new DataNotFoundException(I18n.t("sumaris.error.account.notFound"));
225 }
226
227
228 Preconditions.checkArgument(Objects.equals(existingPerson.getEmail(), account.getEmail()), "Email could not be changed by the user, but only by an administrator.");
229
230
231 account.setProfiles(existingPerson.getProfiles());
232
233
234 account = (AccountVO) personDao.save(account);
235
236
237 UserSettingsVO settings = account.getSettings();
238 if (settings != null) {
239 settings.setIssuer(account.getPubkey());
240 settings = userSettingsDao.save(settings);
241 account.setSettings(settings);
242 }
243
244 return account;
245 }
246
247 @Override
248 public void confirmEmail(String email, String signatureHash) throws InvalidEmailConfirmationException {
249 Preconditions.checkNotNull(email);
250 Preconditions.checkArgument(email.trim().length() > 0);
251 Preconditions.checkNotNull(signatureHash);
252 Preconditions.checkArgument(signatureHash.trim().length() > 0);
253
254 String validSignatureHash = serverCryptoService.hash(serverCryptoService.sign(email.trim()));
255
256
257 PersonFilterVOFilterVO.html#PersonFilterVO">PersonFilterVO filter = new PersonFilterVO();
258 filter.setEmail(email.trim());
259 List<PersonVO> matches = personDao.findByFilter(filter, 0, 2, null, null);
260
261 PersonVO account = null;
262 boolean valid = CollectionUtils.size(matches) == 1 && validSignatureHash.equals(signatureHash);
263
264 if (valid) {
265 account = matches.get(0);
266 valid = account.getStatusId() == config.getStatusIdTemporary();
267
268 if (valid) {
269
270 account.setStatusId(config.getStatusIdValid());
271
272
273 personDao.save(account);
274 }
275 }
276
277 if (!valid) {
278
279 log.warn(I18n.t("sumaris.error.account.register.badEmailOrCode", email));
280 throw new InvalidEmailConfirmationException("Invalid confirmation: bad email or code.");
281 }
282
283
284 log.info(I18n.t("sumaris.server.account.register.confirmed", email));
285
286
287 sendRegistrationToAdmins(account);
288 }
289
290 @Override
291 public void sendConfirmationEmail(String email, String locale) throws InvalidEmailConfirmationException {
292 Preconditions.checkNotNull(email);
293 Preconditions.checkArgument(email.trim().length() > 0);
294
295
296 PersonFilterVOFilterVO.html#PersonFilterVO">PersonFilterVO filter = new PersonFilterVO();
297 filter.setEmail(email.trim());
298 List<PersonVO> matches = personDao.findByFilter(filter, 0, 2, null, null);
299
300 PersonVO account;
301 boolean valid = CollectionUtils.size(matches) == 1;
302
303 if (valid) {
304 account = matches.get(0);
305 valid = account.getStatusId() == config.getStatusIdTemporary();
306
307 if (valid) {
308
309 sendConfirmationLinkByEmail(email, getLocale(locale));
310 }
311 }
312
313 if (!valid) {
314 log.warn(I18n.t("sumaris.error.account.register.sentConfirmation.abort", email));
315 throw new InvalidEmailConfirmationException("Could not sent confirmation email. Unknown email or already confirmed.");
316 }
317
318 }
319
320 @Override
321 public List<Integer> getProfileIdsByPubkey(String pubkey) {
322 PersonVO person = personDao.getByPubkeyOrNull(pubkey);
323 if (person == null) {
324 throw new DataNotFoundException(I18n.t("sumaris.error.person.notFound"));
325 }
326 return person.getProfiles().stream()
327 .map(UserProfileEnum::valueOf)
328 .map(up -> up.id)
329 .collect(Collectors.toList());
330 }
331
332 @Override
333 public List<String> getAllTokensByPubkey(String pubkey) {
334 return userTokenDao.getAllByPubkey(pubkey);
335 }
336
337 @Override
338 public boolean isStoredToken(String token, String pubkey) {
339 return userTokenDao.existsByPubkey(token, pubkey);
340 }
341
342 @Override
343 public void addToken(String token, String pubkey) {
344 userTokenDao.add(token, pubkey);
345 }
346
347 @Override
348 public AccountVO toAccountVO(PersonVO person) {
349 if (person == null) return null;
350
351 AccountVOation/user/AccountVO.html#AccountVO">AccountVO account = new AccountVO();
352 BeanUtils.copyProperties(person, account);
353
354 UserSettingsVO settings = userSettingsDao.getByIssuer(account.getPubkey());
355 account.setSettings(settings);
356
357 return account;
358 }
359
360
361
362
363 protected void checkValid(AccountVO account) {
364 Preconditions.checkNotNull(account);
365 Preconditions.checkNotNull(account.getPubkey(), I18n.t("sumaris.error.validation.required", I18n.t("sumaris.model.person.pubkey")));
366 Preconditions.checkNotNull(account.getEmail(), I18n.t("sumaris.error.validation.required", I18n.t("sumaris.model.person.email")));
367 Preconditions.checkNotNull(account.getFirstName(), I18n.t("sumaris.error.validation.required", I18n.t("sumaris.model.person.firstName")));
368 Preconditions.checkNotNull(account.getLastName(), I18n.t("sumaris.error.validation.required", I18n.t("sumaris.model.person.lastName")));
369 Preconditions.checkNotNull(account.getDepartment(), I18n.t("sumaris.error.validation.required", I18n.t("sumaris.model.person.department")));
370 Preconditions.checkNotNull(account.getDepartment().getId(), I18n.t("sumaris.error.validation.required", I18n.t("sumaris.model.person.department")));
371
372
373 try {
374 new InternetAddress(account.getEmail());
375 } catch (AddressException e) {
376 throw new SumarisTechnicalException(ErrorCodes.BAD_REQUEST, I18n.t("sumaris.error.email.invalid", account.getEmail(), e.getMessage()), e);
377 }
378
379
380 if (account.getSettings() != null) {
381 checkValid(account.getSettings());
382
383
384 if (account.getSettings().getIssuer() != null) {
385 Preconditions.checkArgument(Objects.equals(account.getPubkey(), account.getSettings().getIssuer()), "Bad value for 'settings.issuer' (Should be equals to 'pubkey')");
386 }
387 }
388 }
389
390 protected void checkValid(UserSettingsVO settings) {
391 Preconditions.checkNotNull(settings);
392
393 Preconditions.checkNotNull(settings, I18n.t("sumaris.error.validation.required", I18n.t("sumaris.model.account.settings")));
394 Preconditions.checkNotNull(settings.getLocale(), I18n.t("sumaris.error.validation.required", I18n.t("sumaris.model.settings.locale")));
395 Preconditions.checkNotNull(settings.getLatLongFormat(), I18n.t("sumaris.error.validation.required", I18n.t("sumaris.model.settings.latLongFormat")));
396 }
397
398 private void sendConfirmationLinkByEmail(String toAddress, Locale locale) {
399
400
401 if (this.mailFromAddress != null) {
402 try {
403
404 String signatureHash = serverCryptoService.hash(serverCryptoService.sign(toAddress));
405
406 String confirmationLinkURL = config.getRegistrationConfirmUrlPattern()
407 .replace("{email}", toAddress)
408 .replace("{code}", signatureHash);
409
410 final Email email = DefaultEmail.builder()
411 .from(this.mailFromAddress)
412 .replyTo(this.mailFromAddress)
413 .to(Lists.newArrayList(new InternetAddress(toAddress)))
414 .subject(I18n.l(locale,"sumaris.server.mail.subject.prefix", config.getAppName())
415 + " " + I18n.l(locale, "sumaris.server.account.register.mail.subject"))
416 .body(I18n.l(locale, "sumaris.server.account.register.mail.body",
417 this.serverUrl,
418 confirmationLinkURL,
419 config.getAppName()))
420 .encoding(CHARSET_UTF8.name())
421 .build();
422
423 emailService.send(email);
424 }
425 catch(AddressException e) {
426 throw new SumarisTechnicalException(ErrorCodes.INTERNAL_ERROR, I18n.t("sumaris.error.account.register.sendEmailFailed", e.getMessage()), e);
427 }
428 }
429 }
430
431 private Locale getLocale(String localeStr) {
432 if (localeStr.toLowerCase().startsWith("fr")) {
433 return Locale.FRANCE;
434 }
435 return Locale.UK;
436 }
437
438 protected void sendRegistrationToAdmins(PersonVO confirmedAccount) {
439
440 try {
441
442 List<String> adminEmails = personDao.getEmailsByProfiles(
443 ImmutableList.of(UserProfileEnum.ADMIN.getId()),
444 ImmutableList.of(StatusEnum.ENABLE.getId())
445 );
446
447
448 if (CollectionUtils.isEmpty(adminEmails) || this.mailFromAddress == null) {
449 log.warn("New account registered, but no admin to validate it !");
450 return;
451 }
452
453
454 if (this.mailFromAddress == null) {
455 log.warn("New account registered, but no from address configured to send to administrators!!");
456 return;
457 }
458
459
460
461
462 final Email email = DefaultEmail.builder()
463 .from(this.mailFromAddress)
464 .replyTo(this.mailFromAddress)
465 .to(toInternetAddress(adminEmails))
466 .subject(I18n.t("sumaris.server.mail.subject.prefix", config.getAppName())
467 + " " + I18n.t("sumaris.server.account.register.admin.mail.subject"))
468 .body(I18n.t("sumaris.server.account.register.admin.mail.body",
469 confirmedAccount.getFirstName(),
470 confirmedAccount.getLastName(),
471 confirmedAccount.getEmail(),
472 this.serverUrl,
473 this.serverUrl + "/admin/users",
474 config.getAppName()
475 ))
476 .encoding(CHARSET_UTF8.name())
477 .build();
478
479 emailService.send(email);
480 }
481 catch(Throwable e) {
482
483 log.error(I18n.t("sumaris.error.account.register.sendAdminEmailFailed", e.getMessage()), new SumarisTechnicalException(e));
484 }
485 }
486
487 protected List<InternetAddress> toInternetAddress(List<String> emails) {
488 return emails.stream()
489 .map(email -> {
490 try {
491 return new InternetAddress(email);
492 } catch(AddressException e) {
493 log.debug("Invalid email address {" + email + "}: " + e.getMessage());
494 return null;
495 }
496 })
497 .filter(Objects::nonNull)
498 .collect(Collectors.toList());
499 }
500 }