add support for soft delete of users and changing users passwords.

This commit is contained in:
Keith Woodlock
2012-08-24 12:32:18 +01:00
parent 5f60266921
commit 91890ff204
8 changed files with 54 additions and 73 deletions
@@ -0,0 +1,2 @@
ALTER TABLE `m_appuser`
ADD COLUMN `is_deleted` TINYINT(1) DEFAULT 0 NOT NULL AFTER `id`;
@@ -58,14 +58,14 @@ public class UsersApiResource {
@Autowired @Autowired
private PlatformSecurityContext context; private PlatformSecurityContext context;
private static final Set<String> typicalResponseParameters = new HashSet<String>(Arrays.asList("id", "officeId", "officeName", "username", "firstname", "lastname", "email", "selectedRoles"));
@GET @GET
@Consumes({MediaType.APPLICATION_JSON}) @Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_JSON})
public String retrieveUsers(@Context final UriInfo uriInfo) { public String retrieveUsers(@Context final UriInfo uriInfo) {
Set<String> typicalResponseParameters = new HashSet<String>(Arrays.asList("id", "officeId", "officeName", "username", "firstname", "lastname", "email"));
Set<String> responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters()); Set<String> responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters());
if (responseParameters.isEmpty()) { if (responseParameters.isEmpty()) {
responseParameters.addAll(typicalResponseParameters); responseParameters.addAll(typicalResponseParameters);
@@ -83,8 +83,6 @@ public class UsersApiResource {
@Produces({MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_JSON})
public String retrieveUser(@PathParam("userId") final Long userId, @Context final UriInfo uriInfo) { public String retrieveUser(@PathParam("userId") final Long userId, @Context final UriInfo uriInfo) {
Set<String> typicalResponseParameters = new HashSet<String>(Arrays.asList("id", "officeId", "officeName", "username", "firstname", "lastname", "email"));
Set<String> responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters()); Set<String> responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters());
if (responseParameters.isEmpty()) { if (responseParameters.isEmpty()) {
responseParameters.addAll(typicalResponseParameters); responseParameters.addAll(typicalResponseParameters);
@@ -96,27 +94,29 @@ public class UsersApiResource {
if (template) { if (template) {
List<OfficeLookup> offices = new ArrayList<OfficeLookup>(this.officeReadPlatformService.retrieveAllOfficesForLookup()); List<OfficeLookup> offices = new ArrayList<OfficeLookup>(this.officeReadPlatformService.retrieveAllOfficesForLookup());
user = new AppUserData(user, offices); user = new AppUserData(user, offices);
responseParameters.add("allowedOffices"); addTemplateRelatedParameters(responseParameters);
responseParameters.add("availableRoles");
responseParameters.add("selectedRoles");
} }
return this.apiJsonSerializerService.serializeAppUserDataToJson(prettyPrint, responseParameters, user); return this.apiJsonSerializerService.serializeAppUserDataToJson(prettyPrint, responseParameters, user);
} }
private void addTemplateRelatedParameters(final Set<String> responseParameters) {
responseParameters.add("allowedOffices");
responseParameters.add("availableRoles");
}
@GET @GET
@Path("template") @Path("template")
@Consumes({MediaType.APPLICATION_JSON}) @Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_JSON})
public String newUserDetails(@Context final UriInfo uriInfo) { public String newUserDetails(@Context final UriInfo uriInfo) {
Set<String> typicalResponseParameters = new HashSet<String>(Arrays.asList("id", "officeId", "officeName", "username", "firstname", "lastname", "email",
"allowedOffices", "availableRoles", "selectedRoles"));
Set<String> responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters()); Set<String> responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters());
if (responseParameters.isEmpty()) { if (responseParameters.isEmpty()) {
responseParameters.addAll(typicalResponseParameters); responseParameters.addAll(typicalResponseParameters);
} }
addTemplateRelatedParameters(responseParameters);
boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters()); boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters());
AppUserData user = this.appUserReadPlatformService.retrieveNewUserDetails(); AppUserData user = this.appUserReadPlatformService.retrieveNewUserDetails();
@@ -55,19 +55,4 @@ public class BasicPasswordEncodablePlatformUser implements PlatformUser {
public boolean isEnabled() { public boolean isEnabled() {
return false; return false;
} }
@Override
public boolean isFirstTimeLoginRemaining() {
return false;
}
@Override
public void updateUsernamePasswordOnFirstTimeLogin(final String newUsername, final String newPasswordEncoded) {
// dummy method
}
@Override
public void updatePasswordOnFirstTimeLogin(final String newPasswordEncoded) {
// dummy method
}
} }
@@ -7,9 +7,5 @@ import org.springframework.security.core.userdetails.UserDetails;
*/ */
public interface PlatformUser extends UserDetails { public interface PlatformUser extends UserDetails {
boolean isFirstTimeLoginRemaining(); // void updatePasswordOnFirstTimeLogin(String newPasswordEncoded);
void updateUsernamePasswordOnFirstTimeLogin(String newUsername, String newPasswordEncoded);
void updatePasswordOnFirstTimeLogin(String newPasswordEncoded);
} }
@@ -63,9 +63,11 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
@Embedded @Embedded
private final LoanProductRelatedDetail loanRepaymentScheduleDetail; private final LoanProductRelatedDetail loanRepaymentScheduleDetail;
@SuppressWarnings("unused")
@Column(name = "term_frequency", nullable = false) @Column(name = "term_frequency", nullable = false)
private Integer termFrequency; private Integer termFrequency;
@SuppressWarnings("unused")
@Enumerated(EnumType.ORDINAL) @Enumerated(EnumType.ORDINAL)
@Column(name = "term_period_frequency_enum", nullable = false) @Column(name = "term_period_frequency_enum", nullable = false)
private PeriodFrequencyType termPeriodFrequencyType; private PeriodFrequencyType termPeriodFrequencyType;
@@ -81,6 +83,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
@Column(name = "rejectedon_date") @Column(name = "rejectedon_date")
private Date rejectedOnDate; private Date rejectedOnDate;
@SuppressWarnings("unused")
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
@Column(name = "withdrawnon_date") @Column(name = "withdrawnon_date")
private Date withdrawnOnDate; private Date withdrawnOnDate;
@@ -520,7 +523,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
} }
} }
private void handleLoanOverpayment(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) { private void handleLoanOverpayment(@SuppressWarnings("unused") final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_OVERPAYMENT, LoanStatus.fromInt(this.loanStatus)); LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_OVERPAYMENT, LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue(); this.loanStatus = statusEnum.getValue();
@@ -44,7 +44,7 @@ public class AppUser extends AbstractAuditableCustom<AppUser, Long> implements P
private String password; private String password;
@Column(name = "nonexpired", nullable=false) @Column(name = "nonexpired", nullable=false)
private final boolean accountNonExpired; private boolean accountNonExpired;
@Column(name = "nonlocked", nullable=false) @Column(name = "nonlocked", nullable=false)
private final boolean accountNonLocked; private final boolean accountNonLocked;
@@ -53,10 +53,14 @@ public class AppUser extends AbstractAuditableCustom<AppUser, Long> implements P
private final boolean credentialsNonExpired; private final boolean credentialsNonExpired;
@Column(name = "enabled", nullable=false) @Column(name = "enabled", nullable=false)
private final boolean enabled; private boolean enabled;
@Column(name = "firsttime_login_remaining", nullable=false) @SuppressWarnings("unused")
@Column(name = "firsttime_login_remaining", nullable=false)
private boolean firstTimeLoginRemaining; private boolean firstTimeLoginRemaining;
@Column(name = "is_deleted", nullable=false)
private boolean deleted;
@ManyToOne @ManyToOne
@JoinColumn(name = "office_id") @JoinColumn(name = "office_id")
@@ -65,7 +69,6 @@ public class AppUser extends AbstractAuditableCustom<AppUser, Long> implements P
@ManyToMany(fetch = FetchType.EAGER) @ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "m_appuser_role", joinColumns = @JoinColumn(name = "appuser_id"), inverseJoinColumns = @JoinColumn(name = "role_id")) @JoinTable(name = "m_appuser_role", joinColumns = @JoinColumn(name = "appuser_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles; private Set<Role> roles;
public static AppUser createNew(final Office office, public static AppUser createNew(final Office office,
final Set<Role> allRoles, final String username, final String email, final Set<Role> allRoles, final String username, final String email,
@@ -111,33 +114,9 @@ public class AppUser extends AbstractAuditableCustom<AppUser, Long> implements P
this.firstTimeLoginRemaining = true; this.firstTimeLoginRemaining = true;
} }
@Override
public boolean isFirstTimeLoginRemaining() {
return this.firstTimeLoginRemaining;
}
@Override
public void updateUsernamePasswordOnFirstTimeLogin(final String newUsername,
final String newPasswordEncoded) {
if (this.username.equals(newUsername)) {
throw new UsernameMustBeDifferentException();
}
this.username = newUsername;
updatePasswordOnFirstTimeLogin(newPasswordEncoded);
this.firstTimeLoginRemaining = false;
}
@Override
public void updatePasswordOnFirstTimeLogin(final String newPasswordEncoded) {
if (this.password.equals(newPasswordEncoded)) {
throw new PasswordMustBeDifferentException();
}
this.password = newPasswordEncoded;
this.firstTimeLoginRemaining = false;
}
public void updatePassword(final String encodePassword) { public void updatePassword(final String encodePassword) {
this.password = encodePassword; this.password = encodePassword;
this.firstTimeLoginRemaining = false;
} }
public void update(final Set<Role> allRoles, final Office office, final UserCommand userCommand) { public void update(final Set<Role> allRoles, final Office office, final UserCommand userCommand) {
@@ -162,6 +141,23 @@ public class AppUser extends AbstractAuditableCustom<AppUser, Long> implements P
this.email = userCommand.getEmail(); this.email = userCommand.getEmail();
} }
} }
/**
* Delete is a <i>soft delete</i>. Updates flag so it wont appear in query/report results.
*
* Any fields with unique constraints and prepended with id of record.
*/
public void delete() {
this.deleted = true;
this.enabled = false;
this.accountNonExpired = false;
this.firstTimeLoginRemaining = true;
this.username = this.getId() + "_DELETED_" + this.username;
}
public boolean isDeleted() {
return deleted;
}
@Override @Override
public Collection<GrantedAuthority> getAuthorities() { public Collection<GrantedAuthority> getAuthorities() {
@@ -54,7 +54,7 @@ public class AppUserReadPlatformServiceImpl implements AppUserReadPlatformServic
AppUserMapper mapper = new AppUserMapper(offices); AppUserMapper mapper = new AppUserMapper(offices);
String sql = "select " + mapper.schema() String sql = "select " + mapper.schema()
+ " where u.office_id in (" + officeIdsList + ") order by u.id"; + " where u.office_id in (" + officeIdsList + ") and u.is_deleted = 0 order by u.id";
return this.jdbcTemplate.query(sql, mapper, new Object[] {}); return this.jdbcTemplate.query(sql, mapper, new Object[] {});
} }
@@ -90,7 +90,7 @@ public class AppUserReadPlatformServiceImpl implements AppUserReadPlatformServic
context.authenticatedUser(); context.authenticatedUser();
AppUser user = this.appUserRepository.findOne(userId); AppUser user = this.appUserRepository.findOne(userId);
if (user == null) { if (user == null || user.isDeleted()) {
throw new UserNotFoundException(userId); throw new UserNotFoundException(userId);
} }
@@ -29,7 +29,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
@@ -88,7 +87,7 @@ public class AppUserWritePlatformServiceJpaRepositoryImpl implements AppUserWrit
return appUser.getId(); return appUser.getId();
} catch (DataIntegrityViolationException dve) { } catch (DataIntegrityViolationException dve) {
handleAppUserDataIntegrityIssues(command, dve); handleDataIntegrityIssues(command, dve);
return Long.valueOf(-1); return Long.valueOf(-1);
} catch (PlatformEmailSendException e) { } catch (PlatformEmailSendException e) {
List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>(); List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
@@ -134,13 +133,13 @@ public class AppUserWritePlatformServiceJpaRepositoryImpl implements AppUserWrit
String newPasswordEncoded = this.platformPasswordEncoder.encode(dummyPlatformUser); String newPasswordEncoded = this.platformPasswordEncoder.encode(dummyPlatformUser);
userToUpdate.updatePasswordOnFirstTimeLogin(newPasswordEncoded); userToUpdate.updatePassword(newPasswordEncoded);
this.appUserRepository.saveAndFlush(userToUpdate); this.appUserRepository.saveAndFlush(userToUpdate);
} }
return userToUpdate.getId(); return userToUpdate.getId();
} catch (DataIntegrityViolationException dve) { } catch (DataIntegrityViolationException dve) {
handleAppUserDataIntegrityIssues(command, dve); handleDataIntegrityIssues(command, dve);
return Long.valueOf(-1); return Long.valueOf(-1);
} }
} }
@@ -170,23 +169,23 @@ public class AppUserWritePlatformServiceJpaRepositoryImpl implements AppUserWrit
return allRoles; return allRoles;
} }
// FIXME - KW - move this to a soft delete of user marking a is_deleted field as true so doesnt appear in results
// more than making a user disabled, locked?
@Transactional @Transactional
@Override @Override
public void deleteUser(final Long userId) { public void deleteUser(final Long userId) {
try {
this.appUserRepository.delete(userId); AppUser user = this.appUserRepository.findOne(userId);
} catch (InvalidDataAccessApiUsageException exception) { if (user == null || user.isDeleted()) {
// spring data jpa throws this when deleting user that doesnt exist
throw new UserNotFoundException(userId); throw new UserNotFoundException(userId);
} }
user.delete();
this.appUserRepository.save(user);
} }
/* /*
* Guaranteed to throw an exception no matter what the data integrity issue is. * Guaranteed to throw an exception no matter what the data integrity issue is.
*/ */
private void handleAppUserDataIntegrityIssues(final UserCommand command, final DataIntegrityViolationException dve) { private void handleDataIntegrityIssues(final UserCommand command, final DataIntegrityViolationException dve) {
Throwable realCause = dve.getMostSpecificCause(); Throwable realCause = dve.getMostSpecificCause();
if (realCause.getMessage().contains("username_org")) { if (realCause.getMessage().contains("username_org")) {