diff --git a/mifosng-db/patches/patch-0004-support-soft-delete-of-users.sql b/mifosng-db/patches/patch-0004-support-soft-delete-of-users.sql new file mode 100644 index 000000000..afb3f096a --- /dev/null +++ b/mifosng-db/patches/patch-0004-support-soft-delete-of-users.sql @@ -0,0 +1,2 @@ +ALTER TABLE `m_appuser` +ADD COLUMN `is_deleted` TINYINT(1) DEFAULT 0 NOT NULL AFTER `id`; \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/UsersApiResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/UsersApiResource.java index 03a742496..f2c106124 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/UsersApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/UsersApiResource.java @@ -58,14 +58,14 @@ public class UsersApiResource { @Autowired private PlatformSecurityContext context; + + private static final Set typicalResponseParameters = new HashSet(Arrays.asList("id", "officeId", "officeName", "username", "firstname", "lastname", "email", "selectedRoles")); @GET @Consumes({MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_JSON}) public String retrieveUsers(@Context final UriInfo uriInfo) { - Set typicalResponseParameters = new HashSet(Arrays.asList("id", "officeId", "officeName", "username", "firstname", "lastname", "email")); - Set responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters()); if (responseParameters.isEmpty()) { responseParameters.addAll(typicalResponseParameters); @@ -83,8 +83,6 @@ public class UsersApiResource { @Produces({MediaType.APPLICATION_JSON}) public String retrieveUser(@PathParam("userId") final Long userId, @Context final UriInfo uriInfo) { - Set typicalResponseParameters = new HashSet(Arrays.asList("id", "officeId", "officeName", "username", "firstname", "lastname", "email")); - Set responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters()); if (responseParameters.isEmpty()) { responseParameters.addAll(typicalResponseParameters); @@ -96,27 +94,29 @@ public class UsersApiResource { if (template) { List offices = new ArrayList(this.officeReadPlatformService.retrieveAllOfficesForLookup()); user = new AppUserData(user, offices); - responseParameters.add("allowedOffices"); - responseParameters.add("availableRoles"); - responseParameters.add("selectedRoles"); + addTemplateRelatedParameters(responseParameters); } return this.apiJsonSerializerService.serializeAppUserDataToJson(prettyPrint, responseParameters, user); } + private void addTemplateRelatedParameters(final Set responseParameters) { + responseParameters.add("allowedOffices"); + responseParameters.add("availableRoles"); + } + @GET @Path("template") @Consumes({MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_JSON}) public String newUserDetails(@Context final UriInfo uriInfo) { - Set typicalResponseParameters = new HashSet(Arrays.asList("id", "officeId", "officeName", "username", "firstname", "lastname", "email", - "allowedOffices", "availableRoles", "selectedRoles")); - Set responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters()); if (responseParameters.isEmpty()) { responseParameters.addAll(typicalResponseParameters); } + addTemplateRelatedParameters(responseParameters); + boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters()); AppUserData user = this.appUserReadPlatformService.retrieveNewUserDetails(); diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/BasicPasswordEncodablePlatformUser.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/BasicPasswordEncodablePlatformUser.java index ded4022f0..1232d8c86 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/BasicPasswordEncodablePlatformUser.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/BasicPasswordEncodablePlatformUser.java @@ -55,19 +55,4 @@ public class BasicPasswordEncodablePlatformUser implements PlatformUser { public boolean isEnabled() { 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 - } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformUser.java b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformUser.java index b9ee5658d..65ede6524 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformUser.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/infrastructure/PlatformUser.java @@ -7,9 +7,5 @@ import org.springframework.security.core.userdetails.UserDetails; */ public interface PlatformUser extends UserDetails { - boolean isFirstTimeLoginRemaining(); - - void updateUsernamePasswordOnFirstTimeLogin(String newUsername, String newPasswordEncoded); - - void updatePasswordOnFirstTimeLogin(String newPasswordEncoded); +// void updatePasswordOnFirstTimeLogin(String newPasswordEncoded); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java index 6dde7dfa8..db64a4005 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java @@ -63,9 +63,11 @@ public class Loan extends AbstractAuditableCustom { @Embedded private final LoanProductRelatedDetail loanRepaymentScheduleDetail; + @SuppressWarnings("unused") @Column(name = "term_frequency", nullable = false) private Integer termFrequency; + @SuppressWarnings("unused") @Enumerated(EnumType.ORDINAL) @Column(name = "term_period_frequency_enum", nullable = false) private PeriodFrequencyType termPeriodFrequencyType; @@ -81,6 +83,7 @@ public class Loan extends AbstractAuditableCustom { @Column(name = "rejectedon_date") private Date rejectedOnDate; + @SuppressWarnings("unused") @Temporal(TemporalType.TIMESTAMP) @Column(name = "withdrawnon_date") private Date withdrawnOnDate; @@ -520,7 +523,7 @@ public class Loan extends AbstractAuditableCustom { } } - 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)); this.loanStatus = statusEnum.getValue(); diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/AppUser.java b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/AppUser.java index 16737ef47..f0c0b0969 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/AppUser.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/user/domain/AppUser.java @@ -44,7 +44,7 @@ public class AppUser extends AbstractAuditableCustom implements P private String password; @Column(name = "nonexpired", nullable=false) - private final boolean accountNonExpired; + private boolean accountNonExpired; @Column(name = "nonlocked", nullable=false) private final boolean accountNonLocked; @@ -53,10 +53,14 @@ public class AppUser extends AbstractAuditableCustom implements P private final boolean credentialsNonExpired; @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; + + @Column(name = "is_deleted", nullable=false) + private boolean deleted; @ManyToOne @JoinColumn(name = "office_id") @@ -65,7 +69,6 @@ public class AppUser extends AbstractAuditableCustom implements P @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "m_appuser_role", joinColumns = @JoinColumn(name = "appuser_id"), inverseJoinColumns = @JoinColumn(name = "role_id")) private Set roles; - public static AppUser createNew(final Office office, final Set allRoles, final String username, final String email, @@ -111,33 +114,9 @@ public class AppUser extends AbstractAuditableCustom implements P 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) { this.password = encodePassword; + this.firstTimeLoginRemaining = false; } public void update(final Set allRoles, final Office office, final UserCommand userCommand) { @@ -162,6 +141,23 @@ public class AppUser extends AbstractAuditableCustom implements P this.email = userCommand.getEmail(); } } + + /** + * Delete is a soft delete. 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 public Collection getAuthorities() { diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/user/service/AppUserReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/user/service/AppUserReadPlatformServiceImpl.java index b754044ac..8ec9c6040 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/user/service/AppUserReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/user/service/AppUserReadPlatformServiceImpl.java @@ -54,7 +54,7 @@ public class AppUserReadPlatformServiceImpl implements AppUserReadPlatformServic AppUserMapper mapper = new AppUserMapper(offices); 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[] {}); } @@ -90,7 +90,7 @@ public class AppUserReadPlatformServiceImpl implements AppUserReadPlatformServic context.authenticatedUser(); AppUser user = this.appUserRepository.findOne(userId); - if (user == null) { + if (user == null || user.isDeleted()) { throw new UserNotFoundException(userId); } diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/user/service/AppUserWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/user/service/AppUserWritePlatformServiceJpaRepositoryImpl.java index 078d70166..b7c5c4164 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/user/service/AppUserWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/user/service/AppUserWritePlatformServiceJpaRepositoryImpl.java @@ -29,7 +29,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.ObjectUtils; @@ -88,7 +87,7 @@ public class AppUserWritePlatformServiceJpaRepositoryImpl implements AppUserWrit return appUser.getId(); } catch (DataIntegrityViolationException dve) { - handleAppUserDataIntegrityIssues(command, dve); + handleDataIntegrityIssues(command, dve); return Long.valueOf(-1); } catch (PlatformEmailSendException e) { List dataValidationErrors = new ArrayList(); @@ -134,13 +133,13 @@ public class AppUserWritePlatformServiceJpaRepositoryImpl implements AppUserWrit String newPasswordEncoded = this.platformPasswordEncoder.encode(dummyPlatformUser); - userToUpdate.updatePasswordOnFirstTimeLogin(newPasswordEncoded); + userToUpdate.updatePassword(newPasswordEncoded); this.appUserRepository.saveAndFlush(userToUpdate); } return userToUpdate.getId(); } catch (DataIntegrityViolationException dve) { - handleAppUserDataIntegrityIssues(command, dve); + handleDataIntegrityIssues(command, dve); return Long.valueOf(-1); } } @@ -170,23 +169,23 @@ public class AppUserWritePlatformServiceJpaRepositoryImpl implements AppUserWrit 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 @Override public void deleteUser(final Long userId) { - try { - this.appUserRepository.delete(userId); - } catch (InvalidDataAccessApiUsageException exception) { - // spring data jpa throws this when deleting user that doesnt exist + + AppUser user = this.appUserRepository.findOne(userId); + if (user == null || user.isDeleted()) { throw new UserNotFoundException(userId); } + + user.delete(); + this.appUserRepository.save(user); } /* * 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(); if (realCause.getMessage().contains("username_org")) {