Added user notifications, improved transfer interface, and some refactoring.
This commit is contained in:
parent
2640115e50
commit
095bdc0c74
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
package nl.andrewl.coyotecredit.ctl.dto;
|
||||
|
||||
public record ExchangeAccountData(
|
||||
ExchangeData exchange,
|
||||
SimpleAccountData account
|
||||
) {}
|
|
@ -1,5 +1,7 @@
|
|||
package nl.andrewl.coyotecredit.ctl.dto;
|
||||
|
||||
import nl.andrewl.coyotecredit.ctl.exchange.dto.ExchangeData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
package nl.andrewl.coyotecredit.ctl.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record UserData (
|
||||
long id,
|
||||
String username,
|
||||
String email,
|
||||
List<InvitationData> exchangeInvitations
|
||||
) {}
|
|
@ -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;
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewl.coyotecredit.ctl.dto;
|
||||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
|
||||
|
||||
public record AddSupportedTradeablePayload(long tradeableId) {
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
) {}
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewl.coyotecredit.ctl.dto;
|
||||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
|
||||
|
||||
public record ExchangeData(
|
||||
long id,
|
|
@ -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;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewl.coyotecredit.ctl.dto;
|
||||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
|
||||
|
||||
public record InvitationData(
|
||||
long id,
|
|
@ -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;
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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<InvitationData> exchangeInvitations,
|
||||
List<UserNotificationData> newNotifications
|
||||
) {}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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<UserNotification, Long> {
|
||||
@Query("SELECT un FROM UserNotification un " +
|
||||
"WHERE un.user = :user AND un.dismissed = FALSE " +
|
||||
"ORDER BY un.sentAt DESC")
|
||||
List<UserNotification> findAllNewNotifications(User user);
|
||||
|
||||
@Query("SELECT COUNT(un) FROM UserNotification un " +
|
||||
"WHERE un.user = :user AND un.dismissed = FALSE")
|
||||
long countAllNewNotifications(User user);
|
||||
}
|
|
@ -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<Account> 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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<BalanceData> 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")
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -10,7 +10,15 @@
|
|||
<form th:action="@{/accounts/{aId}/editBalances(aId=${accountId})}" th:method="post">
|
||||
<div class="mb-3" th:each="bal, iter : ${account.balances()}">
|
||||
<label th:for="${'tradeable-' + bal.id()}" class="form-label" th:text="${bal.symbol()}"></label>
|
||||
<input type="number" min="0" th:value="${bal.amount()}" th:name="${'tradeable-' + bal.id()}" class="form-control" required/>
|
||||
<input
|
||||
class="form-control"
|
||||
th:name="${'tradeable-' + bal.id()}"
|
||||
th:value="${bal.amount()}"
|
||||
type="number"
|
||||
min="0"
|
||||
th:step="${bal.type().equals('STOCK') ? '1' : '0.0000000001'}"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">Submit</button>
|
||||
</form>
|
||||
|
|
|
@ -20,10 +20,13 @@
|
|||
<div class="mb-3">
|
||||
<label for="tradeableSelect" class="form-label">Asset</label>
|
||||
<select class="form-select" id="tradeableSelect" name="tradeableId" required>
|
||||
<option value="" selected disabled hidden>Choose something to send</option>
|
||||
<option
|
||||
th:each="b : ${balances}"
|
||||
th:text="${b.symbol() + ' - Balance ' + b.amount()}"
|
||||
th:value="${b.id()}"
|
||||
th:data-amount="${b.amount()}"
|
||||
th:data-type="${b.type()}"
|
||||
></option>
|
||||
</select>
|
||||
</div>
|
||||
|
@ -48,4 +51,6 @@
|
|||
Warning! All transfers are final, and cannot be reversed.
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<script src="/static/js/transfer.js"></script>
|
||||
</div>
|
||||
|
|
|
@ -27,7 +27,14 @@
|
|||
<a class="nav-link" th:href="@{/exchanges}">Exchanges</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" th:href="@{/users/{id}(id=${#authentication.getPrincipal().getId()})}">My Profile</a>
|
||||
<a class="nav-link position-relative" th:href="@{/users/{id}(id=${#authentication.getPrincipal().getId()})}">
|
||||
My Profile
|
||||
<span
|
||||
th:if="${#authentication.getPrincipal().isHasNotifications()}"
|
||||
th:text="${#authentication.getPrincipal().getNewNotificationCount()}"
|
||||
class="badge rounded-pill bg-danger"
|
||||
></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="d-flex" th:action="@{/logout}" th:method="post">
|
||||
|
|
|
@ -20,6 +20,31 @@
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="card text-white bg-dark mb-3">
|
||||
<div class="card-body">
|
||||
<div class="card-title d-flex justify-content-between">
|
||||
<h5>Notifications</h5>
|
||||
<form th:action="@{/userNotifications/dismissAll}" method="post" class="float-end">
|
||||
<button type="submit" class="btn btn-sm btn-secondary">Dismiss All</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item list-group-item-dark" th:each="n : ${user.newNotifications()}">
|
||||
<div class="d-flex justify-content-between">
|
||||
<small th:text="${n.sentAt()}"></small>
|
||||
<form th:action="@{/userNotifications/{nId}/dismiss(nId=${n.id()})}" method="post" class="float-end">
|
||||
<button type="submit" class="btn btn-sm btn-secondary">Dismiss</button>
|
||||
</form>
|
||||
</div>
|
||||
<p th:text="${n.content()}"></p>
|
||||
</li>
|
||||
<li class="list-group-item list-group-item-dark" th:if="${user.newNotifications().isEmpty()}">
|
||||
You don't have any new notifications.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card text-white bg-dark mb-3" th:if="${!user.exchangeInvitations().isEmpty()}">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Exchange Invitations</h5>
|
||||
|
|
Loading…
Reference in New Issue