Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;
import java.util.Random;

Expand All @@ -39,7 +40,7 @@ public AccountController(AccountService accountService, CardService cardService,
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Object createNewAccount(@RequestBody Account account) {
Optional<Account> existing = accountRepository.findByEmailAddress(account.getEmailAddress());
Optional<Account> existing = accountRepository.findByEmailAddressAndNotDeleted(account.getEmailAddress());
if (existing.isPresent()) {
return "User with email address already exists.";
} else {
Expand Down Expand Up @@ -78,7 +79,7 @@ public Object createNewAccount(@RequestBody Account account) {
@GetMapping("/{accountId}")
@ResponseStatus(HttpStatus.OK)
public Account getAccountByAccountId(@PathVariable Long accountId) {
return accountService.getAccountByAccountId(accountId);
return accountService.getAccountByAccountIdWithAuthorization(accountId);
}

//get account of logged-in user
Expand All @@ -98,11 +99,23 @@ public AccountDTO getAccount() {
return accountDTO;
}

@GetMapping("/all")
@ResponseStatus(HttpStatus.OK)
public List<Account> getAllAccounts() {
return accountService.getAllAccountsWithAuthorization();
}

@GetMapping("/deleted")
@ResponseStatus(HttpStatus.OK)
public List<Account> getAllDeletedAccounts() {
return accountService.getAllDeletedAccountsWithAuthorization();
}

//delete
@DeleteMapping("/{accountId}")
@ResponseStatus(HttpStatus.OK)
public Account deleteAccountByAccountId(@PathVariable Long accountId) {
return accountService.deleteAccountByAccountId(accountId);
public Account deleteAccountByAccountId(@PathVariable Long accountId, @RequestParam(required = false) Boolean softDelete) {
return accountService.deleteAccountByAccountIdWithAuthorization(accountId, softDelete);
}

//update
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ public List<Transaction> transactionHistory(@RequestParam(name = "accountId") St
return transactionRepository.findTransactionsByDateCreatedBetweenAndFromAccountIdOrToAccountId(fromDate, toDate, accountId, accountId);
}

@GetMapping("/transactionHistory/accounts/{accountId}")
@ResponseStatus(HttpStatus.OK)
public List<Transaction> getTransactionHistoryForAccount(
@PathVariable Long accountId,
@DateTimeFormat(pattern = "yyyy-MM-dd") @RequestParam(name = "fromDate") Date fromDate,
@DateTimeFormat(pattern = "yyyy-MM-dd") @RequestParam(name = "toDate") Date toDate) {
return transactionService.getTransactionHistoryForAccount(accountId, fromDate, toDate);
}

public Object getErrorMessage() {
throw new UnsupportedOperationException("Unimplemented method 'getErrorMessage'");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ public class AccountDTO {

private Integer totalTransactions;
private Integer totalCards;
private java.util.Set<Role> roles;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,24 @@

import com.hackerrank.corebanking.model.Account;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;

public interface AccountRepository extends JpaRepository<Account, Long> {
Optional<Account> findByEmailAddress(String emailAddress);

@Query("SELECT a FROM Account a WHERE a.deleted = false")
List<Account> findAllActiveAccounts();

@Query("SELECT a FROM Account a WHERE a.emailAddress = :emailAddress AND a.deleted = false")
Optional<Account> findByEmailAddressAndNotDeleted(@Param("emailAddress") String emailAddress);

@Query("SELECT a FROM Account a WHERE a.accountId = :accountId AND a.deleted = false")
Optional<Account> findByIdAndNotDeleted(@Param("accountId") Long accountId);

@Query("SELECT a FROM Account a WHERE a.deleted = true")
List<Account> findAllDeletedAccounts();
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,10 @@ List<Transaction> findBySourceCardNumberAndTransactionDateBetween(
@Param("startDate") Date startDate,
@Param("endDate") Date endDate
);

@Query("SELECT t FROM Transaction t WHERE t.dateCreated BETWEEN :startDate AND :endDate")
List<Transaction> findAllTransactionsByDateRange(
@Param("startDate") Date startDate,
@Param("endDate") Date endDate
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.hackerrank.corebanking.model.Account;
import com.hackerrank.corebanking.model.Role;
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;

@NoArgsConstructor
@AllArgsConstructor
Expand All @@ -26,16 +30,21 @@ public class UserDetailsImpl implements UserDetails {
@JsonIgnore
private String password;

private Set<Role> roles;

public static UserDetailsImpl build(Account user) {
return new UserDetailsImpl(
user.getAccountId(),
user.getEmailAddress(),
user.getPassword());
user.getPassword(),
user.getRoles());
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
return roles.stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,23 @@ public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
AccountRepository accountRepository;

private static final String USER_NOT_FOUND_ERROR_TEMPLATE = "User Not Found with email address: %s";
private static final String ACCOUNT_DELETED_ERROR_TEMPLATE = "Account has been deleted: %s";

@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account user = accountRepository.findByEmailAddress(username)
.orElseThrow(() -> new UsernameNotFoundException("User Not Found with email address: " + username));
public UserDetails loadUserByUsername(String emailAddress) throws UsernameNotFoundException {
Account userAccount = retrieveAccountByEmail(emailAddress);
validateAccountEligibility(userAccount, emailAddress);
return UserDetailsImpl.build(userAccount);
}

private Account retrieveAccountByEmail(String emailAddress) throws UsernameNotFoundException {
return accountRepository.findByEmailAddressAndNotDeleted(emailAddress)
.orElseThrow(() -> new UsernameNotFoundException(
String.format(USER_NOT_FOUND_ERROR_TEMPLATE, emailAddress)));
}

return UserDetailsImpl.build(user);
private void validateAccountEligibility(Account userAccount, String emailAddress) throws UsernameNotFoundException {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth ->
auth.requestMatchers(HttpMethod.POST, "/api/core-banking/auth/**", "/api/core-banking/account/**").permitAll()
.requestMatchers(HttpMethod.GET, "/", "/favicon.ico").permitAll()
auth.requestMatchers(HttpMethod.POST, "/api/core-banking/auth/**", "/api/core-banking/account/**", "/api/core-banking/transaction/**").permitAll()
.requestMatchers(HttpMethod.GET, "/", "/favicon.ico", "/api/core-banking/transaction/**").permitAll()
.anyRequest().authenticated()
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,26 @@
import com.hackerrank.corebanking.model.Account;
import com.hackerrank.corebanking.repository.AccountRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import java.security.Principal;
import java.util.Date;
import java.util.List;

@Service
public class AccountService {

private static final String ADMIN_ROLE = "ADMIN";
private static final String USER_NOT_FOUND_ERROR = "User not found";
private static final String ACCESS_DENIED_ERROR = "Access denied";
private static final String ACCOUNT_NOT_FOUND_ERROR = "Account with given accountId not found.";

private final AccountRepository accountRepository;

@Autowired
AccountService(AccountRepository accountRepository) {
public AccountService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}

Expand All @@ -27,19 +37,19 @@ public Account createNewAccount(Account account) {

public Account getAccountByAccountId(Long accountId) {
return accountRepository
.findById(accountId)
.findByIdAndNotDeleted(accountId)
.orElseThrow(() -> new IllegalArgumentException("Account with given accountId not found."));
}

public Account getAccountByEmailAddress(String emailAddress) {
return accountRepository
.findByEmailAddress(emailAddress)
.findByEmailAddressAndNotDeleted(emailAddress)
.orElseThrow(() -> new IllegalArgumentException("Account with given emailAddress not found."));
}

public Account updateAccountByEmailAddress(Long accountId, Account updatedAccount) {
Account existing = accountRepository
.findById(accountId)
.findByIdAndNotDeleted(accountId)
.orElseThrow(() -> new IllegalArgumentException("Account with given accountId not found."));
existing.setEmailAddress(updatedAccount.getEmailAddress());
accountRepository.save(existing);
Expand All @@ -55,5 +65,69 @@ public Account deleteAccountByAccountId(Long accountId) {

return existing;
}

public Account getAccountByAccountIdWithAuthorization(Long accountId) {
validateAdminAccess();
return getAccountByAccountId(accountId);
}

public Account deleteAccountByAccountIdWithAuthorization(Long accountId, Boolean softDelete) {
validateAdminAccess();

Account accountToDelete = findAccountById(accountId);
return deleteAccount(accountToDelete, softDelete);
}

public List<Account> getAllAccountsWithAuthorization() {
validateAdminAccess();
return accountRepository.findAllActiveAccounts();
}

public List<Account> getAllDeletedAccountsWithAuthorization() {
validateAdminAccess();
return accountRepository.findAllDeletedAccounts();
}

private Account getCurrentAuthenticatedUser() {
UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();

return accountRepository.findByEmailAddressAndNotDeleted(userDetails.getUsername())
.orElseThrow(() -> new AccessDeniedException(USER_NOT_FOUND_ERROR));
}

private boolean hasAdminRole(Account account) {
return account.getRoles().stream()
.anyMatch(role -> ADMIN_ROLE.equals(role.getName()));
}

private void validateAdminAccess() {
Account currentUser = getCurrentAuthenticatedUser();
if (!hasAdminRole(currentUser)) {
throw new AccessDeniedException(ACCESS_DENIED_ERROR);
}
}

private Account findAccountById(Long accountId) {
return accountRepository.findByIdAndNotDeleted(accountId)
.orElseThrow(() -> new IllegalArgumentException(ACCOUNT_NOT_FOUND_ERROR));
}

private Account deleteAccount(Account account, Boolean softDelete) {
return softDelete ?
softDeleteAccount(account) :
hardDeleteAccount(account);
}

private Account softDeleteAccount(Account account) {
account.setDeleted(true);
account.setDeletedAt(new Date());
return accountRepository.save(account);
}

private Account hardDeleteAccount(Account account) {
accountRepository.delete(account);
return account;
}

}
Loading