Commit 0f77a4d3 authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch '252-delete-user-account' into 'master'

Resolve "Delete user account"

Closes #252

See merge request genesys-pgr/genesys-server!149
parents 9edc7fda 0bdca61c
......@@ -97,68 +97,68 @@ dockerize genesys branch:
- tags
deploy for review:
stage: deploy
image: docker:${DOCKER_VERSION}
except:
- master
- /^production-.*/
# when: manual
allow_failure: false
environment:
name: genesys/${CI_COMMIT_REF_SLUG}
url: https://${CI_ENVIRONMENT_SLUG}.review.genesys-pgr.org
on_stop: remove review instance
script:
# Address the swarm
- export DOCKER_HOST=swarm.genesys-pgr.org
# Configuration
- TLS_PATH=~/.docker/${DOCKER_HOST}/certs && mkdir -p ${TLS_PATH}
- echo "${DOCKER_TLS_CA}" > ${TLS_PATH}/ca.pem
- echo "${DOCKER_TLS_KEY}" > ${TLS_PATH}/key.pem
- echo "${DOCKER_TLS_CERT}" > ${TLS_PATH}/cert.pem
- export DOCKER_TLS_VERIFY=1
- export DOCKER_CERT_PATH=${TLS_PATH}
- DOCKER_CMD=docker
# Actions
- apk add --no-cache gettext
- if [ "${CI_COMMIT_REF_SLUG}" = "master" ] ; then
export IMAGE_TAG="${GENESYS_VERSION}";
else
export IMAGE_TAG="${GENESYS_VERSION}-${CI_COMMIT_REF_SLUG}";
fi
- echo Deploying ${CI_REGISTRY_IMAGE}:${IMAGE_TAG} for review as https\://${CI_ENVIRONMENT_SLUG}.review.genesys\-pgr.org
- envsubst < docker/review-compose-template.yml > review-composed.yml
# - cat review-composed.yml
- ${DOCKER_CMD} stack rm ${CI_ENVIRONMENT_SLUG} || true
- ${DOCKER_CMD} stack deploy -c review-composed.yml ${CI_ENVIRONMENT_SLUG}
# deploy for review:
# stage: deploy
# image: docker:${DOCKER_VERSION}
# except:
# - master
# - /^production-.*/
# # when: manual
# allow_failure: false
# environment:
# name: genesys/${CI_COMMIT_REF_SLUG}
# url: https://${CI_ENVIRONMENT_SLUG}.review.genesys-pgr.org
# on_stop: remove review instance
# script:
# # Address the swarm
# - export DOCKER_HOST=swarm.genesys-pgr.org
# # Configuration
# - TLS_PATH=~/.docker/${DOCKER_HOST}/certs && mkdir -p ${TLS_PATH}
# - echo "${DOCKER_TLS_CA}" > ${TLS_PATH}/ca.pem
# - echo "${DOCKER_TLS_KEY}" > ${TLS_PATH}/key.pem
# - echo "${DOCKER_TLS_CERT}" > ${TLS_PATH}/cert.pem
# - export DOCKER_TLS_VERIFY=1
# - export DOCKER_CERT_PATH=${TLS_PATH}
# - DOCKER_CMD=docker
# # Actions
# - apk add --no-cache gettext
# - if [ "${CI_COMMIT_REF_SLUG}" = "master" ] ; then
# export IMAGE_TAG="${GENESYS_VERSION}";
# else
# export IMAGE_TAG="${GENESYS_VERSION}-${CI_COMMIT_REF_SLUG}";
# fi
# - echo Deploying ${CI_REGISTRY_IMAGE}:${IMAGE_TAG} for review as https\://${CI_ENVIRONMENT_SLUG}.review.genesys\-pgr.org
# - envsubst < docker/review-compose-template.yml > review-composed.yml
# # - cat review-composed.yml
# - ${DOCKER_CMD} stack rm ${CI_ENVIRONMENT_SLUG} || true
# - ${DOCKER_CMD} stack deploy -c review-composed.yml ${CI_ENVIRONMENT_SLUG}
remove review instance:
stage: deploy
image: docker:${DOCKER_VERSION}
except:
- master
- /^production-.*/
when: manual
variables:
GIT_STRATEGY: none
environment:
name: genesys/${CI_COMMIT_REF_SLUG}
action: stop
script:
- echo Removing review https\://${CI_ENVIRONMENT_SLUG}.review.genesys\-pgr.org
# Address the swarm
- export DOCKER_HOST=swarm.genesys-pgr.org
# Configuration
- TLS_PATH=~/.docker/${DOCKER_HOST}/certs && mkdir -p ${TLS_PATH}
- echo "${DOCKER_TLS_CA}" > ${TLS_PATH}/ca.pem
- echo "${DOCKER_TLS_KEY}" > ${TLS_PATH}/key.pem
- echo "${DOCKER_TLS_CERT}" > ${TLS_PATH}/cert.pem
- export DOCKER_TLS_VERIFY=1
- export DOCKER_CERT_PATH=${TLS_PATH}
- DOCKER_CMD=docker
# Actions
- ${DOCKER_CMD} stack rm ${CI_ENVIRONMENT_SLUG} || true
# remove review instance:
# stage: deploy
# image: docker:${DOCKER_VERSION}
# except:
# - master
# - /^production-.*/
# when: manual
# variables:
# GIT_STRATEGY: none
# environment:
# name: genesys/${CI_COMMIT_REF_SLUG}
# action: stop
# script:
# - echo Removing review https\://${CI_ENVIRONMENT_SLUG}.review.genesys\-pgr.org
# # Address the swarm
# - export DOCKER_HOST=swarm.genesys-pgr.org
# # Configuration
# - TLS_PATH=~/.docker/${DOCKER_HOST}/certs && mkdir -p ${TLS_PATH}
# - echo "${DOCKER_TLS_CA}" > ${TLS_PATH}/ca.pem
# - echo "${DOCKER_TLS_KEY}" > ${TLS_PATH}/key.pem
# - echo "${DOCKER_TLS_CERT}" > ${TLS_PATH}/cert.pem
# - export DOCKER_TLS_VERIFY=1
# - export DOCKER_CERT_PATH=${TLS_PATH}
# - DOCKER_CMD=docker
# # Actions
# - ${DOCKER_CMD} stack rm ${CI_ENVIRONMENT_SLUG} || true
deploy on staging server:
stage: deploy
......
......@@ -73,12 +73,12 @@ public class CreateContentListener extends RunAsAdminListener {
}
private void createArticles() throws IOException, JsonProcessingException {
if (!createContent) {
LOG.warn("Skipping content creation on startup.");
return;
}
// if (!createContent) {
// LOG.warn("Skipping content creation on startup.");
// return;
// }
LOG.debug("Checking if default content exists");
LOG.info("Checking if default content exists");
final ClassLoader classLoader = CreateContentListener.class.getClassLoader();
final PathMatchingResourcePatternResolver rpr = new PathMatchingResourcePatternResolver(classLoader);
final String resourcePath = "/default-content/*";
......@@ -90,10 +90,18 @@ public class CreateContentListener extends RunAsAdminListener {
final ObjectMapper mapper = new ObjectMapper();
try (InputStream stream = r.getInputStream()) {
final JsonNode json = mapper.readTree(stream);
JsonNode templateNode = json.get("template");
final boolean isTemplate = templateNode != null && templateNode.booleanValue();
final Iterator<Entry<String, JsonNode>> it = json.fields();
while (it.hasNext()) {
final Entry<String, JsonNode> entry = it.next();
if (entry.getKey().length() != 2) {
// Skip over non-language fields
continue;
}
final Locale locale = new Locale(entry.getKey());
// Load from default locale if exists
......@@ -102,10 +110,10 @@ public class CreateContentListener extends RunAsAdminListener {
// If nothing is found, parse the resource and create content
if (article == null) {
try {
contentService.createGlobalArticle(slug, locale, entry.getValue().get("title").asText(), entry.getValue().get("body").asText(), null, false);
LOG.info("Created article for slug: {} lang={}", slug, locale.getLanguage());
contentService.createGlobalArticle(slug, locale, entry.getValue().get("title").asText(), entry.getValue().get("body").asText(), null, isTemplate);
LOG.warn("Created article for slug: {} lang={}", slug, locale.getLanguage());
} catch (CRMException e) {
LOG.warn("Failed to create global article slug={}.", slug, e);
LOG.error("Failed to create global article slug={}.", slug, e);
}
}
}
......
......@@ -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,17 @@ 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;
/**
* Disables the account and removes personally identifiable data.
*
* @param user
* @throws UserException
*/
void archiveUser(User user) 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,54 @@ 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);
}
@Override
@Transactional
public void archiveUser(User user) throws UserException {
user = userRepository.findOne(user.getId());
if (user.hasRole(UserRole.ADMINISTRATOR.getName())) {
throw new UserException("Refusing to disable active administrator account");
}
LOG.warn("Archiving user {}", user.getEmail());
Date now = new Date();
user.setAccountExpires(now);
user.setActive(false);
user.setAccountType(AccountType.LOCAL);
// user.setAccountType(AccountType.DELETED);
user.setEmail("deleted@" + now.getTime());
user.setPassword(THIS_IS_NOT_A_PASSWORD);
user.setFtpPassword(null);
user.setFullName("USER ACCOUNT DELETED");
user.setShortName("deleted" + now.getTime());
user.setPasswordExpires(now);
user.getRoles().clear();
userRepository.save(user);
LOG.warn("Removing ACL entries for {}", user.getEmail());
aclEntryRepository.delete(user.getAclEntries());
}
}
......@@ -42,6 +42,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
......@@ -50,10 +51,12 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
@Controller
@RequestMapping("/content")
......@@ -346,6 +349,28 @@ public class ArticleController extends BaseController {
return "/content/article-edit";
}
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@RequestMapping(value = "/edit/{url}/{language}", produces = MediaType.APPLICATION_JSON_VALUE, params = { "json" })
public @ResponseBody Object articleAsJson(ModelMap model, @PathVariable(value = "url") String slug, @PathVariable("language") String language) {
Article article = contentService.getArticleBySlugAndLang(slug, language);
if (article == null) {
article = new Article();
article.setSlug(slug);
article.setLang(language);
}
model.addAttribute("article", article);
ObjectMapper mapper=new ObjectMapper();
ObjectNode node = mapper.createObjectNode();
node.put("template", article.isTemplate());
ObjectNode lang = node.putObject(language);
lang.put("title", article.getTitle());
lang.put("body", article.getBody());
return node;
}
/**
* Edit article in another language
*
......
......@@ -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) {
......
......@@ -198,6 +198,30 @@ public class UserProfileController extends BaseController {
return "redirect:" + URLBASE + user.getUuid();
}
@RequestMapping(value = "/{uuid:.+}/delete", method = RequestMethod.POST)
public String delete(ModelMap model, @PathVariable("uuid") String uuid) throws UserException {
final User user = userService.getUserByUuid(uuid);
if (user==null) {
throw new ResourceNotFoundException();
}
// if (user.getAccountType() == AccountType.DELETED) {
// LOG.warn("Account already archived.");
// return "redirect:" + VIEWBASE;
// }
if (! user.isAccountNonExpired()) {
LOG.warn("Account already expired.");
return "redirect:" + VIEWBASE;
}
LOG.warn("Archiving user account {}", user.getEmail());
userService.archiveUser(user);
return "redirect:" + VIEWBASE;
}
@RequestMapping(value = "/{uuid:.+}/update-roles", method = { RequestMethod.POST })
public String updateRoles(ModelMap model, @PathVariable("uuid") String uuid, @RequestParam("role") Set<UserRole> selectedRoles) {
final User user = userService.getUserByUuid(uuid);
......
......@@ -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,22 +10,23 @@
<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>