Commit 8d06b5f6 authored by Matija Obreza's avatar Matija Obreza
Browse files

Allow users to delete accounts

- Including default content
- Require authentication for /profile
parent 9edc7fda
......@@ -60,6 +60,11 @@ public interface ContentService {
public final String USER_PASSWORD_RESET = "user-password-reset";
public final String USER_PASSWORD_RESET_EMAIL_SENT = "user-password-reset-email-sent";
public final String USER_RESET_PASSWORD_INSTRUCTIONS = "user-reset-password-instructions";
public final String DELETE_ACCOUNT = "user-delete-account";
public final String SMTP_DELETE_ACCOUNT = "smtp-delete-account";
public final String DELETE_ACCOUNT_SENT = "account-delete-requested";
public final String DELETE_ACCOUNT_CONFIRMED = "account-delete-confirmed";
public final String SMTP_DELETE_ACCOUNT_INPROGRESS = "smtp-delete-account-inprogress";
List<ActivityPost> lastNews();
......
......@@ -16,6 +16,7 @@
package org.genesys2.server.service;
import org.genesys.blocks.security.UserException;
import org.genesys.blocks.security.service.PasswordPolicy.PasswordPolicyException;
import org.genesys2.server.model.impl.User;
import org.genesys2.server.service.TokenVerificationService.NoSuchVerificationTokenException;
......@@ -32,4 +33,8 @@ public interface EMailVerificationService {
void validateEMail(String tokenUuid, String key) throws NoSuchVerificationTokenException, TokenExpiredException;
void changePassword(String tokenUuid, String key, String password) throws NoSuchVerificationTokenException, PasswordPolicyException, TokenExpiredException;
void requestDeleteAccount(User user);
void confirmDeleteAccount(String tokenUuid, String key) throws NoSuchVerificationTokenException, TokenExpiredException, UserException;
}
......@@ -66,4 +66,9 @@ public interface UserService extends BasicUserService<UserRole, User> {
void setFtpPassword(User user, String ftpPassword) throws PasswordPolicyException;
/**
* Disables current user's account
* @throws UserException
*/
void disableMyAccount() throws UserException;
}
......@@ -19,6 +19,8 @@ package org.genesys2.server.service.impl;
import java.text.MessageFormat;
import java.util.Locale;
import org.genesys.blocks.security.SecurityContextUtil;
import org.genesys.blocks.security.UserException;
import org.genesys.blocks.security.service.PasswordPolicy.PasswordPolicyException;
import org.genesys2.server.model.impl.Article;
import org.genesys2.server.model.impl.User;
......@@ -61,6 +63,9 @@ public class EMailVerificationServiceImpl implements EMailVerificationService {
@Value("${base.url}")
private String baseUrl;
@Value("${mail.user.from}")
private String defaultEmailFrom;
@Override
@Transactional
public void sendVerificationEmail(User user) {
......@@ -81,7 +86,7 @@ public class EMailVerificationServiceImpl implements EMailVerificationService {
@Override
@Transactional
public void sendPasswordResetEmail(User user) {
// Generate new token
final VerificationToken verificationToken = tokenVerificationService.generateToken("email-password", user.getUuid());
final Article article = contentService.getGlobalArticle(ContentService.SMTP_EMAIL_PASSWORD, Locale.ENGLISH);
......@@ -132,4 +137,36 @@ public class EMailVerificationServiceImpl implements EMailVerificationService {
}
}
@Override
@Transactional
public void requestDeleteAccount(User user) {
// Generate new token
final VerificationToken verificationToken = tokenVerificationService.generateToken("delete-account", user.getUuid());
final Article article = contentService.getGlobalArticle(ContentService.SMTP_DELETE_ACCOUNT, Locale.ENGLISH);
final String mailBody = MessageFormat.format(article.getBody(), baseUrl, verificationToken.getUuid(), verificationToken.getKey(), user.getFullName(), user.getEmail());
emailService.sendMail(article.getTitle(), mailBody, user.getEmail());
}
@Override
@Transactional(rollbackFor = Throwable.class)
public void confirmDeleteAccount(String tokenUuid, String key) throws NoSuchVerificationTokenException, TokenExpiredException, UserException {
final VerificationToken consumedToken = tokenVerificationService.consumeToken("delete-account", tokenUuid, key);
String uuid = consumedToken.getData();
User currentUser = SecurityContextUtil.getCurrentUser();
if (currentUser.getUuid().equals(uuid)) {
userService.disableMyAccount();
final Article article = contentService.getGlobalArticle(ContentService.SMTP_DELETE_ACCOUNT_INPROGRESS, Locale.ENGLISH);
final String mailBody = MessageFormat.format(article.getBody(), baseUrl, currentUser.getFullName(), currentUser.getEmail());
emailService.sendMail(article.getTitle(), mailBody, defaultEmailFrom, currentUser.getEmail());
} else {
throw new NoSuchVerificationTokenException();
}
}
}
......@@ -18,7 +18,9 @@ package org.genesys2.server.service.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
......@@ -384,4 +386,25 @@ public class UserServiceImpl extends BasicUserServiceImpl<UserRole, User> implem
user.setFtpPassword(passwordEncoder.encode(ftpPassword));
userRepository.save(user);
}
@Override
@Transactional
@PreAuthorize("isFullyAuthenticated()")
public void disableMyAccount() throws UserException {
User currentUser = SecurityContextUtil.getMe();
User u = userRepository.findOne(currentUser.getId());
if (u.hasRole(UserRole.ADMINISTRATOR.getName())) {
throw new UserException("Refusing to disable active administrator account");
}
Calendar expires = Calendar.getInstance();
expires.add(Calendar.MONTH, 1);
u.setAccountExpires(expires.getTime());
u.setActive(false);
// u.setAccountType(AccountType.DELETED);
userRepository.save(u);
}
}
......@@ -17,9 +17,11 @@
package org.genesys2.server.servlet.controller;
import java.io.IOException;
import java.util.Locale;
import java.util.Random;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang.StringUtils;
......@@ -49,7 +51,9 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
......@@ -317,6 +321,38 @@ public class UserProfileController extends BaseController {
return "redirect:/profile/" + user.getUuid();
}
@GetMapping("/me/delete")
@PreAuthorize("isFullyAuthenticated()")
public String startDelete(ModelMap model, Locale locale) {
model.addAttribute("info", contentService.getGlobalArticle(ContentService.DELETE_ACCOUNT, locale));
return "/user/delete";
}
@PostMapping("/me/delete")
@PreAuthorize("isFullyAuthenticated()")
public String requestDelete(ModelMap model) {
final User user = userService.getMe();
if (user == null) {
throw new ResourceNotFoundException();
}
emailVerificationService.requestDeleteAccount(user);
return "redirect:/content/" + ContentService.DELETE_ACCOUNT_SENT;
}
@RequestMapping(value = "/me/delete/{tokenUuid:.+}", method = RequestMethod.GET)
@PreAuthorize("isFullyAuthenticated()")
public String confirmDelete(ModelMap model, HttpServletRequest servletRequest, @PathVariable("tokenUuid") String tokenUuid, @RequestParam(value = "key", required = true) String key) throws NoSuchVerificationTokenException, TokenExpiredException, ServletException, UserException {
emailVerificationService.confirmDeleteAccount(tokenUuid, key);
servletRequest.logout();
return "redirect:/content/" + ContentService.DELETE_ACCOUNT_CONFIRMED;
}
@RequestMapping(value = "/{uuid:.+}/update-roles", method = { RequestMethod.POST })
@PreAuthorize("hasRole('ADMINISTRATOR')")
public String updateRoles(ModelMap model, @PathVariable("uuid") String uuid, @RequestParam("role") Set<UserRole> selectedRoles) {
......
......@@ -128,13 +128,17 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(final HttpSecurity http) throws Exception {
/*@formatter:off*/
http
// No JSESSIONID in URL
.sessionManagement().enableSessionUrlRewriting(false).sessionFixation().migrateSession()
// Authorizations
.and().authorizeRequests().antMatchers("/admin/**", "/1/admin/**").hasRole("ADMINISTRATOR").antMatchers("/profile**", "/oauth/authorize", "/swagger-**").fullyAuthenticated()
.and().authorizeRequests()
// admin
.antMatchers("/admin/**", "/1/admin/**").hasRole("ADMINISTRATOR")
// require login
.antMatchers("/profile", "/profile/**", "/oauth/authorize", "/swagger-**").fullyAuthenticated()
// access denied
.and().exceptionHandling().accessDeniedPage("/access-denied")
......@@ -150,6 +154,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
// Login form
.and().formLogin().permitAll().loginPage("/login").failureUrl("/login?error=1").loginProcessingUrl("/login-attempt").defaultSuccessUrl("/");
/*@formatter:on*/
}
@Override
......
{
"template": false,
"en": {
"title": "Removal of your user account is confirmed",
"body": "<p>We are processing your request to remove your Genesys user account!</p>\r\n<p>The user account is now disabled and you can no longer use it to log into Genesys. All personal information associated with the user account will be removed from Genesys within a grace period of 1 month.</p>"
}
}
{
"template": false,
"en": {
"title": "Confirm removal of your user account",
"body": "<p>A message was sent to your registered email address with a link to confirm your request.</p>\r\n<p>To remove your user account, please:</p>\r\n<ol><li>Remain logged into Genesys</li><li>Check your Inbox and <strong>Spam</strong> folders for the confirmation message</li><li>Open the link in the confirmation message to deactivate your user account and log you out of Genesys</li><li>You are now no longer able to log into Genesys</li><li>A message will be sent to your email address confirming your request to delete the user account and it will be deleted from Genesys after the 1 month grace period.</li></ol>\r\n<p> </p>\r\n<p> </p>"
}
}
{
"template": true,
"en": {
"title": "Genesys user account removal is in progress",
"body": "<p>Dear {1},</p>\r\n<p>We are processing your request to remove your&nbsp;<a href=\"{0}\" target=\"_blank\">Genesys</a>&nbsp;user account <strong>{2}</strong>. Your user account is now disabled and you can no longer use it to log into Genesys.</p>\r\n<p>All personal information will be removed from Genesys within a grace period of 1 month.</p>\r\n<p>&nbsp;</p>\r\n<p>Sorry to see you go,<br />Genesys PGR team</p>"
}
}
{
"template": true,
"en": {
"title": "Remove your Genesys user account",
"body": "<p>Dear {3},</p>\r\n<p>We have received your request to remove your Genesys user account <strong>{4}</strong> from <a title=\"Genesys PGR\" href=\"{0}\" target=\"_blank\">{0}</a> and are sending you this message to confirm the request.</p>\r\n<p><strong>Not you?</strong></p>\r\n<p>Simply ignore and delete this email message.</p>\r\n<p><strong>Remove your Genesys user account</strong></p>\r\n<p>We''re sorry to see you go! To remove your user account please click <a href=\"{0}/profile/me/delete/{1}?key={2}\" target=\"_blank\">{0}/profile/me/delete/{1}?key={2}</a></p>\r\n<p>&nbsp;</p>\r\n<p>Best regards,<br />Genesys PGR team</p>"
}
}
{
"template": false,
"en": {
"title": "Delete user account",
"body": "<p>We&#39;re sorry to see you go. When your user account is deleted from Genesys any personal information associated with the account is removed from Genesys. </p>\r\n<ol><li>Genesys will send a confirmation email to your registered email address to validate account removal request</li><li>The email message contains instructions and a hyperlink to deactivate your user account</li><li>Opening the link in the confirmation message will immediatelly deactivate your account (please note that you must be logged into Genesys)</li><li>Genesys will send you an email confirming that your user account has been deactivated and you can no longer use it to log into Genesys</li><li>After a grace period of 1 month the account is deleted from Genesys</li></ol>\r\n<p>You can always contact us at <a href=\"mailto:helpdesk&#64;genesys-pgr.org\" rel=\"nofollow\">helpdesk&#64;genesys-pgr.org</a> to expedite the process.</p>\r\n<h1>Are you sure? </h1>\r\n<p>Click <strong>Delete</strong> to initiate the account removal process.</p>"
}
}
\ No newline at end of file
......@@ -10,9 +10,10 @@
<table class="table table-striped">
<thead>
<tr>
<th class="col-xs-5"><spring:message code="registration.full-name" /></th>
<th class="col-xs-2"> 1</th>
<th class="col-xs-5"><spring:message code="registration.email" /></th>
<th class="col-xs-3"><spring:message code="registration.full-name" /></th>
<th class="col-xs-3"><spring:message code="registration.email" /></th>
<th class="col-xs-2"><spring:message code="user.account-status" /></th>
<th class="col-xs-2">Expires</th>
</tr>
</thead>
<tbody>
......@@ -22,10 +23,11 @@
<a href="<c:url value="/admin/users/${user.uuid}" />"><c:out
value="${user.fullName}" /></a>
</c:if></td>
<td class="col-xs-5"><c:out value="${user.email}" /></td>
<td class="col-xs-2"><c:if test="${user.accountType == 'SYSTEM'}">SYSTEM</c:if>
<c:if test="${not user.enabled}">DISABLED</c:if> <c:if
test="${user.accountLocked}">LOCKED</c:if></td>
<td class="col-xs-5"><c:out value="${user.email}" /></td>
<td class="col-xs-5"><local:prettyTime date="${user.accountExpires}" locale="${pageContext.response.locale}"/></td>
</tr>
</c:forEach>
</tbody>
......
......@@ -34,6 +34,14 @@
</div>
</div>
<div class="form-group">
<label class="col-lg-2 control-label">Account expires</label>
<div class="col-lg-5 form-control-static">
<c:out value="${user.accountExpires}" />
<local:prettyTime date="${user.accountExpires}" locale="${pageContext.response.locale}"/>
</div>
</div>
<div class="form-group">
<label class="col-lg-2 control-label"><spring:message code="user.roles" /></label>
<div class="col-lg-10 form-control-static">
......
<!DOCTYPE html>
<%@ include file="/WEB-INF/jsp/init.jsp" %>
<html>
<head>
<title><c:out value="${info.title}" /></title>
</head>
<body class="text-page">
<h1 class="green-bg"><c:out value="${info.title}" /></h1>
<div class="row free-text-wrapper">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12"><cms:blurb blurb="${info}" /></div>
</div>
<form method="post" action="">
<input type="hidden" name="${_csrf.parameterName}" value="<c:out value="${_csrf.token}" />"/>
<input class="btn btn-primary" type="submit" value="<spring:message code="delete" />" />
<a href="<c:url value="/profile/${user.uuid}" />" class="btn btn-default">
<spring:message code="cancel" />
</a>
</form>
</body>
</html>
\ No newline at end of file
......@@ -13,6 +13,9 @@
<a href="<c:url value="/profile/${user.uuid}/edit" />" class="btn btn-default pull-right edit-btn">
<spring:message code="edit" />
</a>
<a href="<c:url value="/profile/me/delete" />" class="btn btn-default pull-right edit-btn">
<spring:message code="delete" />
</a>
</security:authorize>
<div class="form-horizontal" id="user-profile-info">
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment