From 095bdc0c74caf9f647050133607c50f6f2b9bc85 Mon Sep 17 00:00:00 2001
From: Andrew Lalis
Date: Fri, 25 Feb 2022 13:40:46 +0100
Subject: [PATCH] Added user notifications, improved transfer interface, and
some refactoring.
---
.../config/UserNotificationSetFilter.java | 36 +++++++++++++++
.../config/WebSecurityConfig.java | 3 ++
.../nl/andrewl/coyotecredit/ctl/HomePage.java | 2 +-
.../ctl/dto/ExchangeAccountData.java | 6 ---
.../coyotecredit/ctl/dto/FullAccountData.java | 2 +
.../coyotecredit/ctl/dto/UserData.java | 10 ----
.../{ => exchange}/ExchangeController.java | 8 ++--
.../dto/AddSupportedTradeablePayload.java | 2 +-
.../dto/EditExchangePayload.java | 2 +-
.../dto/EditTradeablesData.java | 4 +-
.../ctl/exchange/dto/ExchangeAccountData.java | 8 ++++
.../ctl/{ => exchange}/dto/ExchangeData.java | 2 +-
.../{ => exchange}/dto/FullExchangeData.java | 4 +-
.../{ => exchange}/dto/InvitationData.java | 2 +-
.../{ => exchange}/dto/InviteUserPayload.java | 2 +-
.../ctl/user/NotificationController.java | 29 ++++++++++++
.../coyotecredit/ctl/{ => user}/UserPage.java | 3 +-
.../coyotecredit/ctl/user/dto/UserData.java | 13 ++++++
.../ctl/user/dto/UserNotificationData.java | 16 +++++++
.../dao/UserNotificationRepository.java | 21 +++++++++
.../nl/andrewl/coyotecredit/model/User.java | 12 ++++-
.../coyotecredit/model/UserNotification.java | 46 +++++++++++++++++++
.../coyotecredit/service/AccountService.java | 29 +++++++++++-
.../coyotecredit/service/ExchangeService.java | 17 ++++++-
.../coyotecredit/service/UserService.java | 43 ++++++++++++-----
.../application-development.properties | 1 +
src/main/resources/static/js/transfer.js | 19 ++++++++
.../templates/account/edit_balances.html | 10 +++-
.../resources/templates/account/transfer.html | 5 ++
.../resources/templates/fragment/header.html | 9 +++-
src/main/resources/templates/user.html | 25 ++++++++++
31 files changed, 345 insertions(+), 46 deletions(-)
create mode 100644 src/main/java/nl/andrewl/coyotecredit/config/UserNotificationSetFilter.java
delete mode 100644 src/main/java/nl/andrewl/coyotecredit/ctl/dto/ExchangeAccountData.java
delete mode 100644 src/main/java/nl/andrewl/coyotecredit/ctl/dto/UserData.java
rename src/main/java/nl/andrewl/coyotecredit/ctl/{ => exchange}/ExchangeController.java (95%)
rename src/main/java/nl/andrewl/coyotecredit/ctl/{ => exchange}/dto/AddSupportedTradeablePayload.java (56%)
rename src/main/java/nl/andrewl/coyotecredit/ctl/{ => exchange}/dto/EditExchangePayload.java (90%)
rename src/main/java/nl/andrewl/coyotecredit/ctl/{ => exchange}/dto/EditTradeablesData.java (69%)
create mode 100644 src/main/java/nl/andrewl/coyotecredit/ctl/exchange/dto/ExchangeAccountData.java
rename src/main/java/nl/andrewl/coyotecredit/ctl/{ => exchange}/dto/ExchangeData.java (73%)
rename src/main/java/nl/andrewl/coyotecredit/ctl/{ => exchange}/dto/FullExchangeData.java (84%)
rename src/main/java/nl/andrewl/coyotecredit/ctl/{ => exchange}/dto/InvitationData.java (63%)
rename src/main/java/nl/andrewl/coyotecredit/ctl/{ => exchange}/dto/InviteUserPayload.java (77%)
create mode 100644 src/main/java/nl/andrewl/coyotecredit/ctl/user/NotificationController.java
rename src/main/java/nl/andrewl/coyotecredit/ctl/{ => user}/UserPage.java (88%)
create mode 100644 src/main/java/nl/andrewl/coyotecredit/ctl/user/dto/UserData.java
create mode 100644 src/main/java/nl/andrewl/coyotecredit/ctl/user/dto/UserNotificationData.java
create mode 100644 src/main/java/nl/andrewl/coyotecredit/dao/UserNotificationRepository.java
create mode 100644 src/main/java/nl/andrewl/coyotecredit/model/UserNotification.java
create mode 100644 src/main/resources/static/js/transfer.js
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
- My Profile
+
+ My Profile
+
+