diff --git a/src/main/java/nl/andrewl/coyotecredit/config/UserNotificationSetFilter.java b/src/main/java/nl/andrewl/coyotecredit/config/UserNotificationSetFilter.java new file mode 100644 index 0000000..94f23d8 --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/config/UserNotificationSetFilter.java @@ -0,0 +1,36 @@ +package nl.andrewl.coyotecredit.config; + +import lombok.RequiredArgsConstructor; +import nl.andrewl.coyotecredit.dao.UserNotificationRepository; +import nl.andrewl.coyotecredit.model.User; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * A filter that fetches and sets the user's notification count, which is + * displayed in the header for easy checking. + */ +@Component +@RequiredArgsConstructor +public class UserNotificationSetFilter extends OncePerRequestFilter { + private final UserNotificationRepository notificationRepository; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { + User user = (User) auth.getPrincipal(); + user.setNewNotificationCount(notificationRepository.countAllNewNotifications(user)); + user.setHasNotifications(user.getNewNotificationCount() > 0); + } + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/nl/andrewl/coyotecredit/config/WebSecurityConfig.java b/src/main/java/nl/andrewl/coyotecredit/config/WebSecurityConfig.java index 78a55dc..1e20076 100644 --- a/src/main/java/nl/andrewl/coyotecredit/config/WebSecurityConfig.java +++ b/src/main/java/nl/andrewl/coyotecredit/config/WebSecurityConfig.java @@ -9,6 +9,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; @Configuration @EnableWebSecurity @@ -20,6 +21,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final CCUserDetailsService userDetailsService; private final PasswordEncoder passwordEncoder; + private final UserNotificationSetFilter userNotificationSetFilter; @Override protected void configure(HttpSecurity http) throws Exception { @@ -43,6 +45,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { .logoutSuccessUrl("/login") .deleteCookies("JSESSIONID"); + http.addFilterAfter(this.userNotificationSetFilter, SecurityContextHolderAwareRequestFilter.class); } @Override diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/HomePage.java b/src/main/java/nl/andrewl/coyotecredit/ctl/HomePage.java index fb899cb..5ba8cb8 100644 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/HomePage.java +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/HomePage.java @@ -17,7 +17,7 @@ public class HomePage { @GetMapping public String get(Model model, @AuthenticationPrincipal User user) { - model.addAttribute("accounts", accountService.getAccountsOverview(user)); + //model.addAttribute("accounts", accountService.getAccountsOverview(user)); return "home"; } } diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/ExchangeAccountData.java b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/ExchangeAccountData.java deleted file mode 100644 index f93fcc4..0000000 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/ExchangeAccountData.java +++ /dev/null @@ -1,6 +0,0 @@ -package nl.andrewl.coyotecredit.ctl.dto; - -public record ExchangeAccountData( - ExchangeData exchange, - SimpleAccountData account -) {} diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/FullAccountData.java b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/FullAccountData.java index 95b126c..df65b31 100644 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/FullAccountData.java +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/FullAccountData.java @@ -1,5 +1,7 @@ package nl.andrewl.coyotecredit.ctl.dto; +import nl.andrewl.coyotecredit.ctl.exchange.dto.ExchangeData; + import java.util.List; /** diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/UserData.java b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/UserData.java deleted file mode 100644 index b6f0dd5..0000000 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/UserData.java +++ /dev/null @@ -1,10 +0,0 @@ -package nl.andrewl.coyotecredit.ctl.dto; - -import java.util.List; - -public record UserData ( - long id, - String username, - String email, - List exchangeInvitations -) {} diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/ExchangeController.java b/src/main/java/nl/andrewl/coyotecredit/ctl/exchange/ExchangeController.java similarity index 95% rename from src/main/java/nl/andrewl/coyotecredit/ctl/ExchangeController.java rename to src/main/java/nl/andrewl/coyotecredit/ctl/exchange/ExchangeController.java index ab547da..a491221 100644 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/ExchangeController.java +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/exchange/ExchangeController.java @@ -1,9 +1,9 @@ -package nl.andrewl.coyotecredit.ctl; +package nl.andrewl.coyotecredit.ctl.exchange; import lombok.RequiredArgsConstructor; -import nl.andrewl.coyotecredit.ctl.dto.AddSupportedTradeablePayload; -import nl.andrewl.coyotecredit.ctl.dto.EditExchangePayload; -import nl.andrewl.coyotecredit.ctl.dto.InviteUserPayload; +import nl.andrewl.coyotecredit.ctl.exchange.dto.AddSupportedTradeablePayload; +import nl.andrewl.coyotecredit.ctl.exchange.dto.EditExchangePayload; +import nl.andrewl.coyotecredit.ctl.exchange.dto.InviteUserPayload; import nl.andrewl.coyotecredit.model.User; import nl.andrewl.coyotecredit.service.ExchangeService; import org.springframework.data.domain.Pageable; diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/AddSupportedTradeablePayload.java b/src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/AddSupportedTradeablePayload.java similarity index 56% rename from src/main/java/nl/andrewl/coyotecredit/ctl/dto/AddSupportedTradeablePayload.java rename to src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/AddSupportedTradeablePayload.java index d42bed3..00f96bb 100644 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/AddSupportedTradeablePayload.java +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/AddSupportedTradeablePayload.java @@ -1,4 +1,4 @@ -package nl.andrewl.coyotecredit.ctl.dto; +package nl.andrewl.coyotecredit.ctl.exchange.dto; public record AddSupportedTradeablePayload(long tradeableId) { } diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/EditExchangePayload.java b/src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/EditExchangePayload.java similarity index 90% rename from src/main/java/nl/andrewl/coyotecredit/ctl/dto/EditExchangePayload.java rename to src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/EditExchangePayload.java index a4310fd..bfd9d79 100644 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/EditExchangePayload.java +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/EditExchangePayload.java @@ -1,4 +1,4 @@ -package nl.andrewl.coyotecredit.ctl.dto; +package nl.andrewl.coyotecredit.ctl.exchange.dto; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/EditTradeablesData.java b/src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/EditTradeablesData.java similarity index 69% rename from src/main/java/nl/andrewl/coyotecredit/ctl/dto/EditTradeablesData.java rename to src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/EditTradeablesData.java index 4db15c3..e91b1b8 100644 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/EditTradeablesData.java +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/EditTradeablesData.java @@ -1,4 +1,6 @@ -package nl.andrewl.coyotecredit.ctl.dto; +package nl.andrewl.coyotecredit.ctl.exchange.dto; + +import nl.andrewl.coyotecredit.ctl.dto.TradeableData; import java.util.List; diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/ExchangeAccountData.java b/src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/ExchangeAccountData.java new file mode 100644 index 0000000..b33f7a6 --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/ExchangeAccountData.java @@ -0,0 +1,8 @@ +package nl.andrewl.coyotecredit.ctl.exchange.dto; + +import nl.andrewl.coyotecredit.ctl.dto.SimpleAccountData; + +public record ExchangeAccountData( + ExchangeData exchange, + SimpleAccountData account +) {} diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/ExchangeData.java b/src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/ExchangeData.java similarity index 73% rename from src/main/java/nl/andrewl/coyotecredit/ctl/dto/ExchangeData.java rename to src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/ExchangeData.java index 0eba809..bbf2aaf 100644 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/ExchangeData.java +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/ExchangeData.java @@ -1,4 +1,4 @@ -package nl.andrewl.coyotecredit.ctl.dto; +package nl.andrewl.coyotecredit.ctl.exchange.dto; public record ExchangeData( long id, diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/FullExchangeData.java b/src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/FullExchangeData.java similarity index 84% rename from src/main/java/nl/andrewl/coyotecredit/ctl/dto/FullExchangeData.java rename to src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/FullExchangeData.java index eff3a09..f388d6b 100644 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/FullExchangeData.java +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/FullExchangeData.java @@ -1,4 +1,6 @@ -package nl.andrewl.coyotecredit.ctl.dto; +package nl.andrewl.coyotecredit.ctl.exchange.dto; + +import nl.andrewl.coyotecredit.ctl.dto.TradeableData; import java.util.List; diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/InvitationData.java b/src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/InvitationData.java similarity index 63% rename from src/main/java/nl/andrewl/coyotecredit/ctl/dto/InvitationData.java rename to src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/InvitationData.java index b074f9e..71860be 100644 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/InvitationData.java +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/InvitationData.java @@ -1,4 +1,4 @@ -package nl.andrewl.coyotecredit.ctl.dto; +package nl.andrewl.coyotecredit.ctl.exchange.dto; public record InvitationData( long id, diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/InviteUserPayload.java b/src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/InviteUserPayload.java similarity index 77% rename from src/main/java/nl/andrewl/coyotecredit/ctl/dto/InviteUserPayload.java rename to src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/InviteUserPayload.java index e69817f..bc0e149 100644 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/InviteUserPayload.java +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/InviteUserPayload.java @@ -1,4 +1,4 @@ -package nl.andrewl.coyotecredit.ctl.dto; +package nl.andrewl.coyotecredit.ctl.exchange.dto; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/user/NotificationController.java b/src/main/java/nl/andrewl/coyotecredit/ctl/user/NotificationController.java new file mode 100644 index 0000000..44eca4e --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/user/NotificationController.java @@ -0,0 +1,29 @@ +package nl.andrewl.coyotecredit.ctl.user; + +import lombok.RequiredArgsConstructor; +import nl.andrewl.coyotecredit.model.User; +import nl.andrewl.coyotecredit.service.UserService; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping(path = "/userNotifications") +@RequiredArgsConstructor +public class NotificationController { + private final UserService userService; + + @PostMapping(path = "/{notificationId}/dismiss") + public String dismiss(@PathVariable long notificationId, @AuthenticationPrincipal User user) { + userService.dismissNotification(user, notificationId); + return "redirect:/users/" + user.getId(); + } + + @PostMapping(path = "/dismissAll") + public String dismissAll(@AuthenticationPrincipal User user) { + userService.dismissAllNotifications(user); + return "redirect:/users/" + user.getId(); + } +} diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/UserPage.java b/src/main/java/nl/andrewl/coyotecredit/ctl/user/UserPage.java similarity index 88% rename from src/main/java/nl/andrewl/coyotecredit/ctl/UserPage.java rename to src/main/java/nl/andrewl/coyotecredit/ctl/user/UserPage.java index 52e40f5..a18e064 100644 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/UserPage.java +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/user/UserPage.java @@ -1,4 +1,4 @@ -package nl.andrewl.coyotecredit.ctl; +package nl.andrewl.coyotecredit.ctl.user; import lombok.RequiredArgsConstructor; import nl.andrewl.coyotecredit.model.User; @@ -8,6 +8,7 @@ import org.springframework.stereotype.Controller; import org.springframework.ui.Model; 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; @Controller diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/user/dto/UserData.java b/src/main/java/nl/andrewl/coyotecredit/ctl/user/dto/UserData.java new file mode 100644 index 0000000..4b4431a --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/user/dto/UserData.java @@ -0,0 +1,13 @@ +package nl.andrewl.coyotecredit.ctl.user.dto; + +import nl.andrewl.coyotecredit.ctl.exchange.dto.InvitationData; + +import java.util.List; + +public record UserData ( + long id, + String username, + String email, + List exchangeInvitations, + List newNotifications +) {} diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/user/dto/UserNotificationData.java b/src/main/java/nl/andrewl/coyotecredit/ctl/user/dto/UserNotificationData.java new file mode 100644 index 0000000..dd2e22d --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/user/dto/UserNotificationData.java @@ -0,0 +1,16 @@ +package nl.andrewl.coyotecredit.ctl.user.dto; + +import nl.andrewl.coyotecredit.model.UserNotification; + +import java.time.format.DateTimeFormatter; + +public record UserNotificationData( + long id, + String sentAt, + String content, + boolean dismissed +) { + public UserNotificationData(UserNotification n) { + this(n.getId(), n.getSentAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + " UTC", n.getContent(), n.isDismissed()); + } +} diff --git a/src/main/java/nl/andrewl/coyotecredit/dao/UserNotificationRepository.java b/src/main/java/nl/andrewl/coyotecredit/dao/UserNotificationRepository.java new file mode 100644 index 0000000..d5989ed --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/dao/UserNotificationRepository.java @@ -0,0 +1,21 @@ +package nl.andrewl.coyotecredit.dao; + +import nl.andrewl.coyotecredit.model.User; +import nl.andrewl.coyotecredit.model.UserNotification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface UserNotificationRepository extends JpaRepository { + @Query("SELECT un FROM UserNotification un " + + "WHERE un.user = :user AND un.dismissed = FALSE " + + "ORDER BY un.sentAt DESC") + List findAllNewNotifications(User user); + + @Query("SELECT COUNT(un) FROM UserNotification un " + + "WHERE un.user = :user AND un.dismissed = FALSE") + long countAllNewNotifications(User user); +} diff --git a/src/main/java/nl/andrewl/coyotecredit/model/User.java b/src/main/java/nl/andrewl/coyotecredit/model/User.java index e513f30..ddf78b6 100644 --- a/src/main/java/nl/andrewl/coyotecredit/model/User.java +++ b/src/main/java/nl/andrewl/coyotecredit/model/User.java @@ -4,6 +4,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.hibernate.annotations.Formula; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @@ -35,10 +36,12 @@ public class User implements UserDetails { @Column(nullable = false) private String email; - @Column(nullable = false) - @Setter + @Column(nullable = false) @Setter private boolean activated = false; + @Column(nullable = false) @Setter + private boolean admin = false; + @Column(nullable = false, updatable = false) private LocalDateTime createdAt; @@ -48,6 +51,11 @@ public class User implements UserDetails { @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private Set accounts; + @Setter + private transient long newNotificationCount; + @Setter + private transient boolean hasNotifications; + public User(String username, String passwordHash, String email) { this.username = username; this.passwordHash = passwordHash; diff --git a/src/main/java/nl/andrewl/coyotecredit/model/UserNotification.java b/src/main/java/nl/andrewl/coyotecredit/model/UserNotification.java new file mode 100644 index 0000000..7070bc0 --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/model/UserNotification.java @@ -0,0 +1,46 @@ +package nl.andrewl.coyotecredit.model; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.*; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +/** + * Represents a notification that is shown to a user, and can be dismissed once + * the user has read it. + */ +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class UserNotification { + public static final int MAX_LENGTH = 2048; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(optional = false, fetch = FetchType.LAZY) + private User user; + + @Column(nullable = false, updatable = false) + private LocalDateTime sentAt; + + @Column(nullable = false, updatable = false, length = MAX_LENGTH) + private String content; + + @Column(nullable = false) @Setter + private boolean dismissed = false; + + @Column @Setter + private LocalDateTime dismissedAt; + + public UserNotification(User user, String content) { + this.user = user; + this.content = content; + this.sentAt = LocalDateTime.now(ZoneOffset.UTC); + } +} diff --git a/src/main/java/nl/andrewl/coyotecredit/service/AccountService.java b/src/main/java/nl/andrewl/coyotecredit/service/AccountService.java index 3a0577a..75f9660 100644 --- a/src/main/java/nl/andrewl/coyotecredit/service/AccountService.java +++ b/src/main/java/nl/andrewl/coyotecredit/service/AccountService.java @@ -2,6 +2,7 @@ package nl.andrewl.coyotecredit.service; import lombok.RequiredArgsConstructor; import nl.andrewl.coyotecredit.ctl.dto.*; +import nl.andrewl.coyotecredit.ctl.exchange.dto.ExchangeData; import nl.andrewl.coyotecredit.dao.*; import nl.andrewl.coyotecredit.model.*; import nl.andrewl.coyotecredit.util.AccountNumberUtils; @@ -30,6 +31,7 @@ public class AccountService { private final TradeableRepository tradeableRepository; private final TransferRepository transferRepository; private final AccountValueSnapshotRepository valueSnapshotRepository; + private final UserNotificationRepository notificationRepository; @Transactional(readOnly = true) public List getTransferData(long accountId, User user) { @@ -86,13 +88,31 @@ public class AccountService { recipientBalance.setAmount(recipientBalance.getAmount().add(amount)); accountRepository.save(sender); accountRepository.save(recipient); - transferRepository.save(new Transfer( + Transfer t = transferRepository.save(new Transfer( sender.getNumber(), recipient.getNumber(), tradeable, amount, payload.message() )); + String senderMessage = String.format("You have sent %s %s to %s (%s) from your account in %s.", + amount.toPlainString(), + tradeable.getSymbol(), + recipient.getNumber(), + recipient.getName(), + sender.getExchange().getName()); + String recipientMessage = String.format("You have received %s %s from %s (%s) in your account in %s.", + amount.toPlainString(), + tradeable.getSymbol(), + sender.getNumber(), + sender.getName(), + recipient.getExchange().getName()); + if (t.getMessage() != null) { + recipientMessage += " Message: " + t.getMessage(); + senderMessage += " Message: " + t.getMessage(); + } + notificationRepository.save(new UserNotification(sender.getUser(), senderMessage)); + notificationRepository.save(new UserNotification(recipient.getUser(), recipientMessage)); } public static record AccountData ( @@ -175,6 +195,13 @@ public class AccountService { } } accountRepository.save(account); + notificationRepository.save(new UserNotification( + account.getUser(), + String.format("Your account %s in %s had its balances edited by %s.", + account.getNumber(), + account.getExchange().getName(), + userAccount.getName()) + )); } @Scheduled(cron = "@midnight") diff --git a/src/main/java/nl/andrewl/coyotecredit/service/ExchangeService.java b/src/main/java/nl/andrewl/coyotecredit/service/ExchangeService.java index dfbd960..1597a92 100644 --- a/src/main/java/nl/andrewl/coyotecredit/service/ExchangeService.java +++ b/src/main/java/nl/andrewl/coyotecredit/service/ExchangeService.java @@ -2,6 +2,7 @@ package nl.andrewl.coyotecredit.service; import lombok.RequiredArgsConstructor; import nl.andrewl.coyotecredit.ctl.dto.*; +import nl.andrewl.coyotecredit.ctl.exchange.dto.*; import nl.andrewl.coyotecredit.dao.*; import nl.andrewl.coyotecredit.model.*; import nl.andrewl.coyotecredit.util.AccountNumberUtils; @@ -18,7 +19,6 @@ import org.springframework.web.server.ResponseStatusException; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; -import javax.persistence.criteria.Join; import javax.persistence.criteria.Root; import javax.persistence.criteria.Subquery; import java.math.BigDecimal; @@ -39,6 +39,7 @@ public class ExchangeService { private final AccountValueSnapshotRepository accountValueSnapshotRepository; private final UserRepository userRepository; private final ExchangeInvitationRepository invitationRepository; + private final UserNotificationRepository notificationRepository; private final JavaMailSender mailSender; @Value("${coyote-credit.base-url}") @@ -129,6 +130,10 @@ public class ExchangeService { } accountValueSnapshotRepository.deleteAllByAccount(account); accountRepository.delete(account); + notificationRepository.save(new UserNotification( + account.getUser(), + "Your account in " + exchange.getName() + " has been removed." + )); } @Transactional(readOnly = true) @@ -299,6 +304,10 @@ public class ExchangeService { account.getBalances().add(new Balance(account, t, BigDecimal.ZERO)); } accountRepository.save(account); + notificationRepository.save(new UserNotification( + user, + "Congratulations! You've just joined " + exchange.getName() + "." + )); } @Transactional @@ -391,6 +400,12 @@ public class ExchangeService { if (bal != null) { acc.getBalances().remove(bal); accountRepository.save(acc); + notificationRepository.save(new UserNotification( + acc.getUser(), + String.format("Your balance of %s has been removed from your account in %s because the exchange no longer supports it.", + tradeable.getSymbol(), + exchange.getName()) + )); } } exchangeRepository.save(exchange); diff --git a/src/main/java/nl/andrewl/coyotecredit/service/UserService.java b/src/main/java/nl/andrewl/coyotecredit/service/UserService.java index 054d576..6e05891 100644 --- a/src/main/java/nl/andrewl/coyotecredit/service/UserService.java +++ b/src/main/java/nl/andrewl/coyotecredit/service/UserService.java @@ -1,17 +1,12 @@ package nl.andrewl.coyotecredit.service; import lombok.RequiredArgsConstructor; -import nl.andrewl.coyotecredit.ctl.dto.InvitationData; +import nl.andrewl.coyotecredit.ctl.exchange.dto.InvitationData; import nl.andrewl.coyotecredit.ctl.dto.RegisterPayload; -import nl.andrewl.coyotecredit.ctl.dto.UserData; -import nl.andrewl.coyotecredit.dao.AccountRepository; -import nl.andrewl.coyotecredit.dao.ExchangeInvitationRepository; -import nl.andrewl.coyotecredit.dao.UserActivationTokenRepository; -import nl.andrewl.coyotecredit.dao.UserRepository; -import nl.andrewl.coyotecredit.model.Account; -import nl.andrewl.coyotecredit.model.Balance; -import nl.andrewl.coyotecredit.model.User; -import nl.andrewl.coyotecredit.model.UserActivationToken; +import nl.andrewl.coyotecredit.ctl.user.dto.UserData; +import nl.andrewl.coyotecredit.ctl.user.dto.UserNotificationData; +import nl.andrewl.coyotecredit.dao.*; +import nl.andrewl.coyotecredit.model.*; import nl.andrewl.coyotecredit.util.AccountNumberUtils; import nl.andrewl.coyotecredit.util.StringUtils; import org.springframework.beans.factory.annotation.Value; @@ -40,6 +35,7 @@ public class UserService { private final AccountRepository accountRepository; private final UserActivationTokenRepository activationTokenRepository; private final ExchangeInvitationRepository exchangeInvitationRepository; + private final UserNotificationRepository notificationRepository; private final JavaMailSender mailSender; private final PasswordEncoder passwordEncoder; @@ -60,7 +56,10 @@ public class UserService { exchangeInvitations.add(new InvitationData(invitation.getId(), invitation.getExchange().getId(), invitation.getExchange().getName())); } exchangeInvitations.sort(Comparator.comparing(InvitationData::id)); - return new UserData(user.getId(), user.getUsername(), user.getEmail(), exchangeInvitations); + var notifications = notificationRepository.findAllNewNotifications(user).stream() + .map(UserNotificationData::new) + .toList(); + return new UserData(user.getId(), user.getUsername(), user.getEmail(), exchangeInvitations, notifications); } @Transactional @@ -140,4 +139,26 @@ public class UserService { public void removeExpiredActivationTokens() { activationTokenRepository.deleteAllByExpiresAtBefore(LocalDateTime.now(ZoneOffset.UTC)); } + + @Transactional + public void dismissNotification(User user, long notificationId) { + UserNotification n = notificationRepository.findById(notificationId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + if (!n.getUser().getId().equals(user.getId())) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + } + n.setDismissed(true); + n.setDismissedAt(LocalDateTime.now(ZoneOffset.UTC)); + notificationRepository.save(n); + } + + @Transactional + public void dismissAllNotifications(User user) { + var notifications = notificationRepository.findAllNewNotifications(user); + for (var n : notifications) { + n.setDismissed(true); + n.setDismissedAt(LocalDateTime.now(ZoneOffset.UTC)); + } + notificationRepository.saveAll(notifications); + } } diff --git a/src/main/resources/application-development.properties b/src/main/resources/application-development.properties index 0e3eebb..f7edcc9 100644 --- a/src/main/resources/application-development.properties +++ b/src/main/resources/application-development.properties @@ -4,6 +4,7 @@ spring.datasource.password=tester spring.jpa.hibernate.ddl-auto=update spring.jpa.open-in-view=false +spring.jpa.show-sql=false spring.mail.host=127.0.0.1 spring.mail.port=1025 diff --git a/src/main/resources/static/js/transfer.js b/src/main/resources/static/js/transfer.js new file mode 100644 index 0000000..84d0daf --- /dev/null +++ b/src/main/resources/static/js/transfer.js @@ -0,0 +1,19 @@ +const tradeableSelect = document.getElementById("tradeableSelect"); +const valueInput = document.getElementById("amountInput"); + +tradeableSelect.addEventListener("change", onSelectChanged); + +function onSelectChanged() { + valueInput.value = null; + const option = tradeableSelect.options[tradeableSelect.selectedIndex]; + const type = option.dataset.type; + const balance = Number(option.dataset.amount); + valueInput.setAttribute("max", "" + balance); + if (type === "STOCK") { + valueInput.setAttribute("step", 1); + valueInput.setAttribute("min", 1); + } else { + valueInput.setAttribute("step", 0.0000000001); + valueInput.setAttribute("min", 0.0000000001); + } +} diff --git a/src/main/resources/templates/account/edit_balances.html b/src/main/resources/templates/account/edit_balances.html index d470d06..39ce8dd 100644 --- a/src/main/resources/templates/account/edit_balances.html +++ b/src/main/resources/templates/account/edit_balances.html @@ -10,7 +10,15 @@
- +
diff --git a/src/main/resources/templates/account/transfer.html b/src/main/resources/templates/account/transfer.html index d4b58bb..a402028 100644 --- a/src/main/resources/templates/account/transfer.html +++ b/src/main/resources/templates/account/transfer.html @@ -20,10 +20,13 @@
@@ -48,4 +51,6 @@ Warning! All transfers are final, and cannot be reversed.

+ + diff --git a/src/main/resources/templates/fragment/header.html b/src/main/resources/templates/fragment/header.html index 2a487ce..c4f3fc7 100644 --- a/src/main/resources/templates/fragment/header.html +++ b/src/main/resources/templates/fragment/header.html @@ -27,7 +27,14 @@ Exchanges
diff --git a/src/main/resources/templates/user.html b/src/main/resources/templates/user.html index c70cd3f..a84971e 100644 --- a/src/main/resources/templates/user.html +++ b/src/main/resources/templates/user.html @@ -20,6 +20,31 @@ +
+
+
+
Notifications
+ + + +
+
+
    +
  • +
    + +
    + +
    +
    +

    +
  • +
  • + You don't have any new notifications. +
  • +
+
+
Exchange Invitations