View Javadoc
1   package net.sumaris.server.service.administration;
2   
3   /*-
4    * #%L
5    * SUMARiS:: Server
6    * %%
7    * Copyright (C) 2018 SUMARiS Consortium
8    * %%
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU General Public License as
11   * published by the Free Software Foundation, either version 3 of the
12   * License, or (at your option) any later version.
13   * 
14   * This program is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Public License for more details.
18   * 
19   * You should have received a copy of the GNU General Public
20   * License along with this program.  If not, see
21   * <http://www.gnu.org/licenses/gpl-3.0.html>.
22   * #L%
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      /* Logger */
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; // loop back to force transactional handling
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         // Get mail 'from'
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         // Get server URL
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         // Check if valid
167         checkValid(account);
168 
169         // Check if not already exists
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         // Generate confirmation code
178 
179 
180         // Skip mail confirmation
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             // Mark account as temporary
187             account.setStatusId(config.getStatusIdTemporary());
188         }
189 
190         // Set default profile
191         account.setProfiles(Lists.newArrayList(UserProfileEnum.GUEST.label));
192 
193         // Normalize email
194         account.setEmail(org.apache.commons.lang3.StringUtils.trimToNull(account.getEmail()));
195 
196         // Save account
197         AccountVO../../net/sumaris/core/vo/administration/user/AccountVO.html#AccountVO">AccountVO savedAccount = (AccountVO) personDao.save(account);
198 
199         // Save settings
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         // Send confirmation Email
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         // Check if valid
218         checkValid(account);
219         Preconditions.checkNotNull(account.getId());
220 
221         // Get existing account
222         PersonVO existingPerson = personDao.get(account.getId());
223         if (existingPerson == null) {
224             throw new DataNotFoundException(I18n.t("sumaris.error.account.notFound"));
225         }
226 
227         // Check same email
228         Preconditions.checkArgument(Objects.equals(existingPerson.getEmail(), account.getEmail()), "Email could not be changed by the user, but only by an administrator.");
229 
230         // Make sure to restore existing profiles, to avoid any changes by the user himself
231         account.setProfiles(existingPerson.getProfiles());
232 
233         // Do the save
234         account = (AccountVO) personDao.save(account);
235 
236         // Save settings
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         // Mark account as temporary
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         // Check the matched account status
264         if (valid) {
265             account = matches.get(0);
266             valid = account.getStatusId() == config.getStatusIdTemporary();
267 
268             if (valid) {
269                 // Mark account status as valid
270                 account.setStatusId(config.getStatusIdValid());
271 
272                 // Save account
273                 personDao.save(account);
274             }
275         }
276 
277         if (!valid) {
278             // Log success
279             log.warn(I18n.t("sumaris.error.account.register.badEmailOrCode", email));
280             throw new InvalidEmailConfirmationException("Invalid confirmation: bad email or code.");
281         }
282 
283         // Log success
284         log.info(I18n.t("sumaris.server.account.register.confirmed", email));
285 
286         // Send email to admins
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         // Mark account as temporary
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         // Check the matched account status
303         if (valid) {
304             account = matches.get(0);
305             valid = account.getStatusId() == config.getStatusIdTemporary();
306 
307             if (valid) {
308                 // Sent the confirmation email
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     /* -- protected methods -- */
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         // Check email
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         // Check settings and settings.locale
380         if (account.getSettings() != null) {
381             checkValid(account.getSettings());
382 
383             // Check settings issuer
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         // Check settings and settings.locale
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         // Send confirmation Email
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             // No admin: log on server
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             // No from address: could not send email
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             // TODO: group email by locales (get it with the email, from personService)
460 
461             // Send the email
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             // Just log, but continue
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 }