diff --git a/.gitignore b/.gitignore index 549e00a..de3a887 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ build/ ### VS Code ### .vscode/ + +/config/ diff --git a/src/main/java/nl/andrewl/coyotecredit/config/SchedulingConfig.java b/src/main/java/nl/andrewl/coyotecredit/config/SchedulingConfig.java new file mode 100644 index 0000000..13139b9 --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/config/SchedulingConfig.java @@ -0,0 +1,9 @@ +package nl.andrewl.coyotecredit.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@Configuration +@EnableScheduling +public class SchedulingConfig { +} diff --git a/src/main/java/nl/andrewl/coyotecredit/config/WebSecurityConfig.java b/src/main/java/nl/andrewl/coyotecredit/config/WebSecurityConfig.java index 05f7109..78a55dc 100644 --- a/src/main/java/nl/andrewl/coyotecredit/config/WebSecurityConfig.java +++ b/src/main/java/nl/andrewl/coyotecredit/config/WebSecurityConfig.java @@ -26,7 +26,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { http .authorizeRequests() .antMatchers( - "/login", "/login/processing", "/static/**" + "/login*", "/login/processing", "/register*", "/activate*", "/static/**" ).permitAll() .and() .authorizeRequests().anyRequest().authenticated() diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/AccountPage.java b/src/main/java/nl/andrewl/coyotecredit/ctl/AccountPage.java index dffe0d5..9696ea5 100644 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/AccountPage.java +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/AccountPage.java @@ -1,7 +1,7 @@ package nl.andrewl.coyotecredit.ctl; -import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.RequiredArgsConstructor; +import nl.andrewl.coyotecredit.ctl.dto.TransferPayload; import nl.andrewl.coyotecredit.model.User; import nl.andrewl.coyotecredit.service.AccountService; import org.springframework.http.HttpStatus; @@ -12,8 +12,6 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; -import java.util.Map; - @Controller @RequestMapping(path = "/accounts/{accountId}") @RequiredArgsConstructor @@ -44,4 +42,16 @@ public class AccountPage { accountService.editBalances(accountId, user, paramMap); return "redirect:/accounts/" + accountId; } + + @GetMapping(path = "/transfer") + public String getTransferPage(Model model, @PathVariable long accountId, @AuthenticationPrincipal User user) { + model.addAttribute("balances", accountService.getTransferData(accountId, user)); + return "account/transfer"; + } + + @PostMapping(path = "/transfer") + public String postTransfer(@PathVariable long accountId, @ModelAttribute TransferPayload payload, @AuthenticationPrincipal User user) { + accountService.transfer(accountId, user, payload); + return "redirect:/accounts/" + accountId; + } } diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/ExchangePage.java b/src/main/java/nl/andrewl/coyotecredit/ctl/ExchangeController.java similarity index 76% rename from src/main/java/nl/andrewl/coyotecredit/ctl/ExchangePage.java rename to src/main/java/nl/andrewl/coyotecredit/ctl/ExchangeController.java index 6d9811f..f53d370 100644 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/ExchangePage.java +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/ExchangeController.java @@ -10,42 +10,48 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; @Controller -@RequestMapping(path = "/exchanges/{exchangeId}") +@RequestMapping(path = "/exchanges") @RequiredArgsConstructor -public class ExchangePage { +public class ExchangeController { private final ExchangeService exchangeService; @GetMapping - public String get(Model model, @PathVariable long exchangeId, @AuthenticationPrincipal User user) { - model.addAttribute("exchange", exchangeService.getData(exchangeId, user)); - return "exchange"; + public String getExchanges(Model model, @AuthenticationPrincipal User user) { + model.addAttribute("exchangeData", exchangeService.getExchanges(user)); + return "exchange/exchanges"; } - @GetMapping(path = "/accounts") + @GetMapping(path = "/{exchangeId}") + public String get(Model model, @PathVariable long exchangeId, @AuthenticationPrincipal User user) { + model.addAttribute("exchange", exchangeService.getData(exchangeId, user)); + return "exchange/exchange"; + } + + @GetMapping(path = "/{exchangeId}/accounts") public String getAccountsPage(Model model, @PathVariable long exchangeId, @AuthenticationPrincipal User user) { model.addAttribute("accounts", exchangeService.getAccounts(exchangeId, user)); return "exchange/accounts"; } - @GetMapping(path = "/addAccount") + @GetMapping(path = "/{exchangeId}/addAccount") public String getAddAccountPage(@PathVariable long exchangeId, @AuthenticationPrincipal User user) { exchangeService.ensureAdminAccount(exchangeId, user); return "exchange/addAccount"; } - @PostMapping(path = "/addAccount") + @PostMapping(path = "/{exchangeId}/addAccount") public String postAddAcount(@PathVariable long exchangeId, @AuthenticationPrincipal User user, @ModelAttribute AddAccountPayload payload) { long accountId = exchangeService.addAccount(exchangeId, user, payload); return "redirect:/accounts/" + accountId; } - @GetMapping(path = "/removeAccount/{accountId}") + @GetMapping(path = "/{exchangeId}/removeAccount/{accountId}") public String getRemoveAccountPage(@PathVariable long exchangeId, @PathVariable long accountId, @AuthenticationPrincipal User user) { exchangeService.ensureAdminAccount(exchangeId, user); return "exchange/removeAccount"; } - @PostMapping(path = "/removeAccount/{accountId}") + @PostMapping(path = "/{exchangeId}/removeAccount/{accountId}") public String postRemoveAccount(@PathVariable long exchangeId, @PathVariable long accountId, @AuthenticationPrincipal User user) { exchangeService.removeAccount(exchangeId, accountId, user); return "redirect:/exchanges/" + exchangeId + "/accounts"; diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/LoginPage.java b/src/main/java/nl/andrewl/coyotecredit/ctl/LoginPage.java deleted file mode 100644 index fb7a7d2..0000000 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/LoginPage.java +++ /dev/null @@ -1,14 +0,0 @@ -package nl.andrewl.coyotecredit.ctl; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; - -@Controller -@RequestMapping(path = "/login") -public class LoginPage { - @GetMapping - public String get() { - return "login"; - } -} diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/PublicPageController.java b/src/main/java/nl/andrewl/coyotecredit/ctl/PublicPageController.java new file mode 100644 index 0000000..4602e4c --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/PublicPageController.java @@ -0,0 +1,38 @@ +package nl.andrewl.coyotecredit.ctl; + +import lombok.RequiredArgsConstructor; +import nl.andrewl.coyotecredit.ctl.dto.RegisterPayload; +import nl.andrewl.coyotecredit.service.UserService; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Controller +@RequiredArgsConstructor +public class PublicPageController { + private final UserService userService; + + @GetMapping(path = "/login") + public String getLoginPage() { + return "public/login"; + } + + @GetMapping(path = "/register") + public String getRegisterPage() { + return "public/register"; + } + + @PostMapping(path = "/register") + public String postRegister(@ModelAttribute RegisterPayload payload) { + userService.registerUser(payload); + return "redirect:/login"; + } + + @GetMapping(path = "/activate") + public String activateAccount(@RequestParam(name = "token") String token) { + userService.activateUser(token); + return "redirect:/login"; + } +} diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/AddAccountPayload.java b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/AddAccountPayload.java index e37d89a..ee451e4 100644 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/AddAccountPayload.java +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/AddAccountPayload.java @@ -2,6 +2,7 @@ package nl.andrewl.coyotecredit.ctl.dto; public record AddAccountPayload( String name, + String email, String username, String password ) {} diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/BalanceData.java b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/BalanceData.java index bf34450..c3ec7b0 100644 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/BalanceData.java +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/BalanceData.java @@ -3,5 +3,6 @@ package nl.andrewl.coyotecredit.ctl.dto; public record BalanceData( long id, String symbol, + String type, String amount ) {} diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/ExchangeAccountData.java b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/ExchangeAccountData.java new file mode 100644 index 0000000..f93fcc4 --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/ExchangeAccountData.java @@ -0,0 +1,6 @@ +package nl.andrewl.coyotecredit.ctl.dto; + +public record ExchangeAccountData( + ExchangeData exchange, + SimpleAccountData account +) {} diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/FullExchangeData.java b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/FullExchangeData.java index 2f47d42..d434559 100644 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/FullExchangeData.java +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/FullExchangeData.java @@ -10,7 +10,7 @@ public record FullExchangeData ( String name, TradeableData primaryTradeable, List supportedTradeables, - // Account info - boolean accountAdmin -) { -} + // Account info that's needed for determining if it's possible to do some actions. + boolean accountAdmin, + long accountId +) {} diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/RegisterPayload.java b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/RegisterPayload.java new file mode 100644 index 0000000..5e5f83b --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/RegisterPayload.java @@ -0,0 +1,7 @@ +package nl.andrewl.coyotecredit.ctl.dto; + +public record RegisterPayload ( + String username, + String email, + String password +) {} diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/TradeableData.java b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/TradeableData.java index 36aa911..31e3ca5 100644 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/TradeableData.java +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/TradeableData.java @@ -2,20 +2,26 @@ package nl.andrewl.coyotecredit.ctl.dto; import nl.andrewl.coyotecredit.model.Tradeable; +import java.text.DecimalFormat; + public record TradeableData( long id, String symbol, String type, String marketPriceUsd, + String formattedPriceUsd, String name, String description ) { + public static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,##0.00"); + public TradeableData(Tradeable t) { this( t.getId(), t.getSymbol(), t.getType().name(), t.getMarketPriceUsd().toPlainString(), + DECIMAL_FORMAT.format(t.getMarketPriceUsd()), t.getName(), t.getDescription() ); diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/TransferPayload.java b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/TransferPayload.java new file mode 100644 index 0000000..ac7a16e --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/TransferPayload.java @@ -0,0 +1,8 @@ +package nl.andrewl.coyotecredit.ctl.dto; + +public record TransferPayload ( + String recipientNumber, + String amount, + long tradeableId, + String message +) {} diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/UserData.java b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/UserData.java index 75d8746..9c22094 100644 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/UserData.java +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/UserData.java @@ -2,5 +2,6 @@ package nl.andrewl.coyotecredit.ctl.dto; public record UserData ( long id, - String username + String username, + String email ) {} diff --git a/src/main/java/nl/andrewl/coyotecredit/dao/TradeableRepository.java b/src/main/java/nl/andrewl/coyotecredit/dao/TradeableRepository.java index 4dd4016..7502f1a 100644 --- a/src/main/java/nl/andrewl/coyotecredit/dao/TradeableRepository.java +++ b/src/main/java/nl/andrewl/coyotecredit/dao/TradeableRepository.java @@ -4,6 +4,9 @@ import nl.andrewl.coyotecredit.model.Tradeable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface TradeableRepository extends JpaRepository { + List findAllByExchangeNull(); } diff --git a/src/main/java/nl/andrewl/coyotecredit/dao/TransferRepository.java b/src/main/java/nl/andrewl/coyotecredit/dao/TransferRepository.java new file mode 100644 index 0000000..e8ea028 --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/dao/TransferRepository.java @@ -0,0 +1,18 @@ +package nl.andrewl.coyotecredit.dao; + +import nl.andrewl.coyotecredit.model.Transfer; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface TransferRepository extends JpaRepository { + @Query( + "SELECT t FROM Transfer t " + + "WHERE t.senderNumber = :accountNumber OR t.recipientNumber = :accountNumber " + + "ORDER BY t.timestamp DESC" + ) + Page findAllForAccount(String accountNumber, Pageable pageable); +} diff --git a/src/main/java/nl/andrewl/coyotecredit/dao/UserActivationTokenRepository.java b/src/main/java/nl/andrewl/coyotecredit/dao/UserActivationTokenRepository.java new file mode 100644 index 0000000..49a421a --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/dao/UserActivationTokenRepository.java @@ -0,0 +1,12 @@ +package nl.andrewl.coyotecredit.dao; + +import nl.andrewl.coyotecredit.model.UserActivationToken; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; + +@Repository +public interface UserActivationTokenRepository extends JpaRepository { + void deleteAllByExpiresAtBefore(LocalDateTime time); +} diff --git a/src/main/java/nl/andrewl/coyotecredit/dao/UserRepository.java b/src/main/java/nl/andrewl/coyotecredit/dao/UserRepository.java index c626303..75e9229 100644 --- a/src/main/java/nl/andrewl/coyotecredit/dao/UserRepository.java +++ b/src/main/java/nl/andrewl/coyotecredit/dao/UserRepository.java @@ -9,4 +9,5 @@ import java.util.Optional; @Repository public interface UserRepository extends JpaRepository { Optional findByUsername(String username); + boolean existsByUsername(String username); } diff --git a/src/main/java/nl/andrewl/coyotecredit/model/CustomTradeable.java b/src/main/java/nl/andrewl/coyotecredit/model/CustomTradeable.java deleted file mode 100644 index 4a0d5e5..0000000 --- a/src/main/java/nl/andrewl/coyotecredit/model/CustomTradeable.java +++ /dev/null @@ -1,20 +0,0 @@ -package nl.andrewl.coyotecredit.model; - -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.ManyToOne; - -@Entity -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Getter -public class CustomTradeable extends Tradeable { - /** - * The exchange that the tradeable belongs to. - */ - @ManyToOne(optional = false, fetch = FetchType.LAZY) - private Exchange exchange; -} diff --git a/src/main/java/nl/andrewl/coyotecredit/model/Exchange.java b/src/main/java/nl/andrewl/coyotecredit/model/Exchange.java index 727d897..e17f3a5 100644 --- a/src/main/java/nl/andrewl/coyotecredit/model/Exchange.java +++ b/src/main/java/nl/andrewl/coyotecredit/model/Exchange.java @@ -47,7 +47,7 @@ public class Exchange { * The set of custom tradeables created specifically for use in this exchange. */ @OneToMany(mappedBy = "exchange", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) - private Set customTradeables; + private Set customTradeables; /** * The set of accounts that are registered with this exchange. diff --git a/src/main/java/nl/andrewl/coyotecredit/model/Tradeable.java b/src/main/java/nl/andrewl/coyotecredit/model/Tradeable.java index c35ce20..42b26b2 100644 --- a/src/main/java/nl/andrewl/coyotecredit/model/Tradeable.java +++ b/src/main/java/nl/andrewl/coyotecredit/model/Tradeable.java @@ -3,6 +3,7 @@ package nl.andrewl.coyotecredit.model; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import javax.persistence.*; import java.math.BigDecimal; @@ -29,6 +30,7 @@ public class Tradeable { private TradeableType type; @Column(nullable = false, precision = 24, scale = 10) + @Setter private BigDecimal marketPriceUsd = new BigDecimal(1); @Column(nullable = false) @@ -37,12 +39,19 @@ public class Tradeable { @Column private String description; - public Tradeable(String symbol, TradeableType type, String name, String description, BigDecimal marketPriceUsd) { + /** + * The exchange that this tradeable belongs to, if any. + */ + @ManyToOne(fetch = FetchType.LAZY) + private Exchange exchange; + + public Tradeable(String symbol, TradeableType type, String name, String description, BigDecimal marketPriceUsd, Exchange exchange) { this.symbol = symbol; this.type = type; this.name = name; this.description = description; this.marketPriceUsd = marketPriceUsd; + this.exchange = exchange; } @Override diff --git a/src/main/java/nl/andrewl/coyotecredit/model/Transfer.java b/src/main/java/nl/andrewl/coyotecredit/model/Transfer.java new file mode 100644 index 0000000..2d9446a --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/model/Transfer.java @@ -0,0 +1,49 @@ +package nl.andrewl.coyotecredit.model; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +/** + * Represents a transfer of funds from one account to another. + */ +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class Transfer { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, updatable = false) + private LocalDateTime timestamp; + + @Column(nullable = false) + private String senderNumber; + + @Column(nullable = false) + private String recipientNumber; + + @ManyToOne(optional = false, fetch = FetchType.EAGER) + private Tradeable tradeable; + + @Column(nullable = false, precision = 24, scale = 10) + private BigDecimal amount; + + @Column(length = 1024) + private String message; + + public Transfer(String sender, String recipient, Tradeable tradeable, BigDecimal amount, String message) { + this.senderNumber = sender; + this.recipientNumber = recipient; + this.tradeable = tradeable; + this.amount = amount; + this.message = message; + this.timestamp = LocalDateTime.now(ZoneOffset.UTC); + } +} diff --git a/src/main/java/nl/andrewl/coyotecredit/model/User.java b/src/main/java/nl/andrewl/coyotecredit/model/User.java index 9a2ec69..e513f30 100644 --- a/src/main/java/nl/andrewl/coyotecredit/model/User.java +++ b/src/main/java/nl/andrewl/coyotecredit/model/User.java @@ -3,10 +3,13 @@ package nl.andrewl.coyotecredit.model; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import javax.persistence.*; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -29,15 +32,27 @@ public class User implements UserDetails { @Column(nullable = false) private String passwordHash; + @Column(nullable = false) + private String email; + + @Column(nullable = false) + @Setter + private boolean activated = false; + + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; + /** * The set of accounts this user has. */ @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private Set accounts; - public User(String username, String passwordHash) { + public User(String username, String passwordHash, String email) { this.username = username; this.passwordHash = passwordHash; + this.email = email; + this.createdAt = LocalDateTime.now(ZoneOffset.UTC); this.accounts = new HashSet<>(); } @@ -75,6 +90,6 @@ public class User implements UserDetails { @Override public boolean isEnabled() { - return true; + return this.activated; } } diff --git a/src/main/java/nl/andrewl/coyotecredit/model/UserActivationToken.java b/src/main/java/nl/andrewl/coyotecredit/model/UserActivationToken.java new file mode 100644 index 0000000..c4cde51 --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/model/UserActivationToken.java @@ -0,0 +1,32 @@ +package nl.andrewl.coyotecredit.model; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.time.LocalDateTime; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class UserActivationToken { + @Id + @Column(nullable = false, unique = true, updatable = false) + private String token; + + @ManyToOne(optional = false, fetch = FetchType.EAGER) + private User user; + + /** + * The time at which this token expires, in UTC. + */ + @Column(nullable = false) + private LocalDateTime expiresAt; + + public UserActivationToken(String token, User user, LocalDateTime expiresAt) { + this.token = token; + this.user = user; + this.expiresAt = expiresAt; + } +} diff --git a/src/main/java/nl/andrewl/coyotecredit/service/AccountService.java b/src/main/java/nl/andrewl/coyotecredit/service/AccountService.java index c08f350..bd2ed8c 100644 --- a/src/main/java/nl/andrewl/coyotecredit/service/AccountService.java +++ b/src/main/java/nl/andrewl/coyotecredit/service/AccountService.java @@ -1,17 +1,12 @@ package nl.andrewl.coyotecredit.service; import lombok.RequiredArgsConstructor; -import nl.andrewl.coyotecredit.ctl.dto.BalanceData; -import nl.andrewl.coyotecredit.ctl.dto.ExchangeData; -import nl.andrewl.coyotecredit.ctl.dto.FullAccountData; -import nl.andrewl.coyotecredit.ctl.dto.TransactionData; +import nl.andrewl.coyotecredit.ctl.dto.*; import nl.andrewl.coyotecredit.dao.AccountRepository; import nl.andrewl.coyotecredit.dao.TradeableRepository; import nl.andrewl.coyotecredit.dao.TransactionRepository; -import nl.andrewl.coyotecredit.model.Account; -import nl.andrewl.coyotecredit.model.Balance; -import nl.andrewl.coyotecredit.model.Tradeable; -import nl.andrewl.coyotecredit.model.User; +import nl.andrewl.coyotecredit.dao.TransferRepository; +import nl.andrewl.coyotecredit.model.*; import org.springframework.data.domain.PageRequest; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; @@ -29,6 +24,61 @@ public class AccountService { private final AccountRepository accountRepository; private final TransactionRepository transactionRepository; private final TradeableRepository tradeableRepository; + private final TransferRepository transferRepository; + + @Transactional(readOnly = true) + public List getTransferData(long accountId, User user) { + Account account = accountRepository.findById(accountId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + if (!account.getUser().getId().equals(user.getId())) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + } + return account.getBalances().stream() + .filter(b -> b.getAmount().compareTo(BigDecimal.ZERO) > 0) + .map(b -> new BalanceData( + b.getTradeable().getId(), + b.getTradeable().getSymbol(), + b.getTradeable().getType().name(), + b.getAmount().toPlainString() + )) + .sorted(Comparator.comparing(BalanceData::symbol)) + .toList(); + } + + @Transactional + public void transfer(long accountId, User user, TransferPayload payload) { + Account sender = accountRepository.findById(accountId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + if (!sender.getUser().getId().equals(user.getId())) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + } + Account recipient = accountRepository.findByNumber(payload.recipientNumber()) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown recipient.")); + Tradeable tradeable = tradeableRepository.findById(payload.tradeableId()) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown tradeable asset.")); + BigDecimal amount = new BigDecimal(payload.amount()); + if (amount.compareTo(BigDecimal.ZERO) <= 0) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid amount. Should be positive."); + Balance senderBalance = sender.getBalanceForTradeable(tradeable); + if (senderBalance == null || senderBalance.getAmount().compareTo(amount) < 0) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Not enough funds to transfer."); + } + Balance recipientBalance = recipient.getBalanceForTradeable(tradeable); + if (recipientBalance == null) { + recipientBalance = new Balance(recipient, tradeable, BigDecimal.ZERO); + recipient.getBalances().add(recipientBalance); + } + senderBalance.setAmount(senderBalance.getAmount().subtract(amount)); + recipientBalance.setAmount(recipientBalance.getAmount().add(amount)); + accountRepository.save(sender); + accountRepository.save(recipient); + transferRepository.save(new Transfer( + sender.getNumber(), + recipient.getNumber(), + tradeable, + amount, + payload.message() + )); + } public static record AccountData ( long id, @@ -71,10 +121,15 @@ public class AccountService { account.getExchange().getPrimaryTradeable().getSymbol() ), account.getBalances().stream() - .map(b -> new BalanceData(b.getTradeable().getId(), b.getTradeable().getSymbol(), b.getAmount().toPlainString())) + .map(b -> new BalanceData( + b.getTradeable().getId(), + b.getTradeable().getSymbol(), + b.getTradeable().getType().name(), + b.getAmount().toPlainString() + )) .sorted(Comparator.comparing(BalanceData::symbol)) .toList(), - account.getTotalBalance().toPlainString(), + TradeableData.DECIMAL_FORMAT.format(account.getTotalBalance()), transactionData ); } diff --git a/src/main/java/nl/andrewl/coyotecredit/service/ExchangeService.java b/src/main/java/nl/andrewl/coyotecredit/service/ExchangeService.java index d95c95a..9b33431 100644 --- a/src/main/java/nl/andrewl/coyotecredit/service/ExchangeService.java +++ b/src/main/java/nl/andrewl/coyotecredit/service/ExchangeService.java @@ -45,7 +45,8 @@ public class ExchangeService { .map(TradeableData::new) .sorted(Comparator.comparing(TradeableData::symbol)) .toList(), - account.isAdmin() + account.isAdmin(), + account.getId() ); } @@ -91,7 +92,7 @@ public class ExchangeService { if (!account.isAdmin()) { throw new ResponseStatusException(HttpStatus.NOT_FOUND); } - User u = userRepository.save(new User(payload.username(), passwordEncoder.encode(payload.password()))); + User u = userRepository.save(new User(payload.username(), passwordEncoder.encode(payload.password()), payload.email())); Account a = accountRepository.save(new Account( AccountNumberUtils.generate(), u, @@ -188,4 +189,15 @@ public class ExchangeService { Transaction tx = new Transaction(account.getNumber(), exchange, from, fromValue, to, toValue, LocalDateTime.now(ZoneOffset.UTC)); transactionRepository.save(tx); } + + @Transactional(readOnly = true) + public List getExchanges(User user) { + return accountRepository.findAllByUser(user).stream() + .map(a -> new ExchangeAccountData( + new ExchangeData(a.getExchange().getId(), a.getExchange().getName(), a.getExchange().getPrimaryTradeable().getSymbol()), + new SimpleAccountData(a.getId(), a.getNumber(), a.getName(), a.isAdmin(), TradeableData.DECIMAL_FORMAT.format(a.getTotalBalance())) + )) + .sorted(Comparator.comparing(d -> d.exchange().name())) + .toList(); + } } diff --git a/src/main/java/nl/andrewl/coyotecredit/service/TradeableUpdateService.java b/src/main/java/nl/andrewl/coyotecredit/service/TradeableUpdateService.java new file mode 100644 index 0000000..604b9f0 --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/service/TradeableUpdateService.java @@ -0,0 +1,155 @@ +package nl.andrewl.coyotecredit.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import nl.andrewl.coyotecredit.dao.TradeableRepository; +import nl.andrewl.coyotecredit.model.Tradeable; +import nl.andrewl.coyotecredit.model.TradeableType; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.math.BigDecimal; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Service +@RequiredArgsConstructor +@Slf4j +public class TradeableUpdateService { + private final TradeableRepository tradeableRepository; + + private final HttpClient httpClient = HttpClient.newHttpClient(); + private final ObjectMapper objectMapper = new ObjectMapper(); + private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); + + @Value("${coyote-credit.polygon.api-key}") + private String polygonApiKey; + private static final int POLYGON_API_TIMEOUT = 15; + + @Scheduled(cron = "@midnight") + public void updatePublicTradeables() { + List publicTradeables = tradeableRepository.findAllByExchangeNull(); + long delay = 5; + for (var tradeable : publicTradeables) { + // Special case of ignoring USD as the universal transfer currency. + if (tradeable.getSymbol().equals("USD")) continue; + executorService.schedule(() -> updateTradeable(tradeable), delay, TimeUnit.SECONDS); + delay += POLYGON_API_TIMEOUT; + } + } + + private void updateTradeable(Tradeable tradeable) { + BigDecimal updatedValue = null; + if (tradeable.getType().equals(TradeableType.STOCK)) { + updatedValue = fetchStockClosePrice(tradeable.getSymbol()); + } else if (tradeable.getType().equals(TradeableType.CRYPTO)) { + updatedValue = fetchCryptoClosePrice(tradeable.getSymbol()); + } else if (tradeable.getType().equals(TradeableType.FIAT)) { + updatedValue = fetchForexClosePrice(tradeable.getSymbol()); + } + if (updatedValue != null) { + log.info( + "Updating market price for tradeable {} ({}, {}) from {} to {}.", + tradeable.getId(), + tradeable.getSymbol(), + tradeable.getName(), + tradeable.getMarketPriceUsd().toPlainString(), + updatedValue.toPlainString() + ); + tradeable.setMarketPriceUsd(updatedValue); + tradeableRepository.save(tradeable); + } + } + + private BigDecimal fetchStockClosePrice(String symbol) { + String url = String.format("https://api.polygon.io/v2/aggs/ticker/%s/prev?adjusted=true", symbol); + HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + .GET() + .header("Authorization", "Bearer " + polygonApiKey) + .build(); + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray()); + if (response.statusCode() == 200) { + ObjectNode data = objectMapper.readValue(response.body(), ObjectNode.class); + JsonNode resultsCount = data.get("resultsCount"); + if (resultsCount != null && resultsCount.isIntegralNumber() && resultsCount.asInt() > 0) { + String closePriceText = data.withArray("results").get(0).get("c").asText(); + return new BigDecimal(closePriceText); + } else { + throw new IOException("No results were returned."); + } + } else { + throw new IOException("Request returned a non-200 status: " + response.statusCode()); + } + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + return null; + } + } + + private BigDecimal fetchCryptoClosePrice(String symbol) { + String date = LocalDate.now(ZoneOffset.UTC).minusDays(1).format(DateTimeFormatter.ISO_LOCAL_DATE); + String url = String.format("https://api.polygon.io/v1/open-close/crypto/%s/USD/%s?adjusted=true", symbol, date); + HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + .GET() + .header("Authorization", "Bearer " + polygonApiKey) + .build(); + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray()); + if (response.statusCode() == 200) { + ObjectNode data = objectMapper.readValue(response.body(), ObjectNode.class); + JsonNode close = data.get("close"); + if (close != null && close.isNumber()) { + return new BigDecimal(close.asText()); + } else { + throw new IOException("No data available."); + } + } else { + throw new IOException("Request returned a non-200 status: " + response.statusCode()); + } + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + return null; + } + } + + private BigDecimal fetchForexClosePrice(String symbol) { + String url = String.format("https://api.polygon.io/v2/aggs/ticker/C:%sUSD/prev?adjusted=true", symbol); + HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + .GET() + .header("Authorization", "Bearer " + polygonApiKey) + .build(); + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray()); + if (response.statusCode() == 200) { + ObjectNode data = objectMapper.readValue(response.body(), ObjectNode.class); + JsonNode resultsCount = data.get("resultsCount"); + if (resultsCount != null && resultsCount.isIntegralNumber() && resultsCount.asInt() > 0) { + String closePriceText = data.withArray("results").get(0).get("c").asText(); + return new BigDecimal(closePriceText); + } else { + throw new IOException("No results were returned."); + } + } else { + throw new IOException("Request returned a non-200 status: " + response.statusCode()); + } + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/src/main/java/nl/andrewl/coyotecredit/service/UserService.java b/src/main/java/nl/andrewl/coyotecredit/service/UserService.java index 8ebd95c..cde61eb 100644 --- a/src/main/java/nl/andrewl/coyotecredit/service/UserService.java +++ b/src/main/java/nl/andrewl/coyotecredit/service/UserService.java @@ -1,20 +1,40 @@ package nl.andrewl.coyotecredit.service; import lombok.RequiredArgsConstructor; +import nl.andrewl.coyotecredit.ctl.dto.RegisterPayload; import nl.andrewl.coyotecredit.ctl.dto.UserData; +import nl.andrewl.coyotecredit.dao.UserActivationTokenRepository; import nl.andrewl.coyotecredit.dao.UserRepository; import nl.andrewl.coyotecredit.model.User; +import nl.andrewl.coyotecredit.model.UserActivationToken; +import nl.andrewl.coyotecredit.util.StringUtils; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.server.ResponseStatusException; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + @Service @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; + private final UserActivationTokenRepository activationTokenRepository; + private final JavaMailSender mailSender; + private final PasswordEncoder passwordEncoder; - @Transactional + @Value("${coyote-credit.base-url}") + private String baseUrl; + + @Transactional(readOnly = true) public UserData getUser(long userId, User requestingUser) { User user; if (requestingUser.getId().equals(userId)) { @@ -22,6 +42,62 @@ public class UserService { } else { user = userRepository.findById(userId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } - return new UserData(user.getId(), user.getUsername()); + return new UserData(user.getId(), user.getUsername(), user.getEmail()); + } + + @Transactional + public void registerUser(RegisterPayload payload) { + if (userRepository.existsByUsername(payload.username())) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Username is already taken."); + } + User user = new User(payload.username(), passwordEncoder.encode(payload.password()), payload.email()); + user = userRepository.save(user); + String token = StringUtils.random(64); + LocalDateTime expiresAt = LocalDateTime.now(ZoneOffset.UTC).plusHours(24); + UserActivationToken activationToken = new UserActivationToken(token, user, expiresAt); + activationTokenRepository.save(activationToken); + try { + sendActivationEmail(activationToken); + } catch (MessagingException e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Could not send activation email."); + } + } + + @Transactional + public void activateUser(String tokenString) { + UserActivationToken token = activationTokenRepository.findById(tokenString) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid activation code.")); + if (token.getExpiresAt().isBefore(LocalDateTime.now(ZoneOffset.UTC))) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Activation code is expired."); + } + token.getUser().setActivated(true); + activationTokenRepository.delete(token); + userRepository.save(token.getUser()); + } + + private void sendActivationEmail(UserActivationToken token) throws MessagingException { + MimeMessage msg = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(msg); + helper.setFrom("Coyote Credit "); + helper.setTo(token.getUser().getEmail()); + helper.setSubject("Activate Your Account"); + String activationUrl = baseUrl + "/activate?token=" + token.getToken(); + helper.setText(String.format( + """ +

In order to complete your account registration for Coyote Credit, + please follow this link:

+ %s. +

Note that this link will expire in 24 hours.

+

If you did not register for an account, or you are unaware of + someone registering on your behalf, you may safely ignore this + email.

""", + activationUrl, activationUrl + ), true); + mailSender.send(msg); + } + + @Scheduled(cron = "@midnight") + public void removeExpiredActivationTokens() { + activationTokenRepository.deleteAllByExpiresAtBefore(LocalDateTime.now(ZoneOffset.UTC)); } } diff --git a/src/main/java/nl/andrewl/coyotecredit/util/StringUtils.java b/src/main/java/nl/andrewl/coyotecredit/util/StringUtils.java new file mode 100644 index 0000000..f1002a9 --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/util/StringUtils.java @@ -0,0 +1,17 @@ +package nl.andrewl.coyotecredit.util; + +import java.security.SecureRandom; +import java.util.Random; + +public class StringUtils { + private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"; + + public static String random(int length) { + StringBuilder sb = new StringBuilder(length); + Random rand = new SecureRandom(); + for (int i = 0; i < length; i++) { + sb.append(ALPHABET.charAt(rand.nextInt(ALPHABET.length()))); + } + return sb.toString(); + } +} diff --git a/src/main/resources/application-development.properties b/src/main/resources/application-development.properties index 6c4995a..0e3eebb 100644 --- a/src/main/resources/application-development.properties +++ b/src/main/resources/application-development.properties @@ -4,3 +4,8 @@ spring.datasource.password=tester spring.jpa.hibernate.ddl-auto=update spring.jpa.open-in-view=false + +spring.mail.host=127.0.0.1 +spring.mail.port=1025 + +coyote-credit.base-url=http://localhost:8080 diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css index e69de29..63fa55d 100644 --- a/src/main/resources/static/css/style.css +++ b/src/main/resources/static/css/style.css @@ -0,0 +1,31 @@ +@font-face { + font-family: SpaceMono; + src: url("/static/font/SpaceMono-Regular.ttf"); + font-style: normal; + font-weight: normal; +} + +@font-face { + font-family: SpaceMono; + src: url("/static/font/SpaceMono-Italic.ttf"); + font-style: italic; + font-weight: normal; +} + +@font-face { + font-family: SpaceMono; + src: url("/static/font/SpaceMono-Bold.ttf"); + font-style: normal; + font-weight: bold; +} + +@font-face { + font-family: SpaceMono; + src: url("/static/font/SpaceMono-BoldItalic.ttf"); + font-style: italic; + font-weight: bold; +} + +.currency { + font-family: SpaceMono, monospace; +} diff --git a/src/main/resources/static/font/SpaceMono-Bold.ttf b/src/main/resources/static/font/SpaceMono-Bold.ttf new file mode 100644 index 0000000..14aab33 Binary files /dev/null and b/src/main/resources/static/font/SpaceMono-Bold.ttf differ diff --git a/src/main/resources/static/font/SpaceMono-BoldItalic.ttf b/src/main/resources/static/font/SpaceMono-BoldItalic.ttf new file mode 100644 index 0000000..3124efb Binary files /dev/null and b/src/main/resources/static/font/SpaceMono-BoldItalic.ttf differ diff --git a/src/main/resources/static/font/SpaceMono-Italic.ttf b/src/main/resources/static/font/SpaceMono-Italic.ttf new file mode 100644 index 0000000..eb15c27 Binary files /dev/null and b/src/main/resources/static/font/SpaceMono-Italic.ttf differ diff --git a/src/main/resources/static/font/SpaceMono-Regular.ttf b/src/main/resources/static/font/SpaceMono-Regular.ttf new file mode 100644 index 0000000..d713495 Binary files /dev/null and b/src/main/resources/static/font/SpaceMono-Regular.ttf differ diff --git a/src/main/resources/templates/account.html b/src/main/resources/templates/account.html index 654f633..eeb1f99 100644 --- a/src/main/resources/templates/account.html +++ b/src/main/resources/templates/account.html @@ -8,28 +8,35 @@

Account

In

-

Total value of

+

Total value of  

Overview

- + + - + +
CurrencyAssetType Balance
-

Recent Transactions

- - + Trade + Transfer + Edit Balances + +
+

Recent Transactions

+
+ @@ -37,18 +44,16 @@ - - + + - + - + - -
From Amount FromAmount To Timestamp
- - Trade - Edit Balances + + +
\ No newline at end of file diff --git a/src/main/resources/templates/account/edit_balances.html b/src/main/resources/templates/account/edit_balances.html index 04dd210..3b459fc 100644 --- a/src/main/resources/templates/account/edit_balances.html +++ b/src/main/resources/templates/account/edit_balances.html @@ -10,7 +10,7 @@
- +
diff --git a/src/main/resources/templates/account/transfer.html b/src/main/resources/templates/account/transfer.html new file mode 100644 index 0000000..fb033ab --- /dev/null +++ b/src/main/resources/templates/account/transfer.html @@ -0,0 +1,33 @@ + + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
diff --git a/src/main/resources/templates/exchange/accounts.html b/src/main/resources/templates/exchange/accounts.html index e93a02f..8bf902f 100644 --- a/src/main/resources/templates/exchange/accounts.html +++ b/src/main/resources/templates/exchange/accounts.html @@ -7,8 +7,6 @@

Accounts

- Add Account - @@ -23,9 +21,11 @@ - +
Remove
+ + Add Account
diff --git a/src/main/resources/templates/exchange/addAccount.html b/src/main/resources/templates/exchange/addAccount.html index 39427e3..eeb65f2 100644 --- a/src/main/resources/templates/exchange/addAccount.html +++ b/src/main/resources/templates/exchange/addAccount.html @@ -13,15 +13,19 @@
- +
- + +
+
+ +
- +
diff --git a/src/main/resources/templates/exchange.html b/src/main/resources/templates/exchange/exchange.html similarity index 60% rename from src/main/resources/templates/exchange.html rename to src/main/resources/templates/exchange/exchange.html index fb501d5..ff52c5f 100644 --- a/src/main/resources/templates/exchange.html +++ b/src/main/resources/templates/exchange/exchange.html @@ -8,16 +8,16 @@

- Primary tradeable: + Primary asset:

-

Supported Tradeable Currencies / Stocks

+

Tradeable Assets

- + @@ -25,15 +25,14 @@ - +
Symbol TypePrice ($)Price (in USD) Name
- \ No newline at end of file diff --git a/src/main/resources/templates/exchange/exchanges.html b/src/main/resources/templates/exchange/exchanges.html new file mode 100644 index 0000000..bf81500 --- /dev/null +++ b/src/main/resources/templates/exchange/exchanges.html @@ -0,0 +1,34 @@ + + +
+

Exchanges

+ + + + + + + + + + + + + + + + + +
NamePrimary AssetAccountEstimated Balance
+ + + +
+

+ Use this page to view a list of all exchanges you're participating in. Click on the name of the exchange to view its page, and click on your account number to view your account information for a given exchange. The estimated balance shown in this overview may not be completely accurate. Navigate to your account page for a complete overview of your current account balances for stocks, fiat currencies, and more. +

+
\ No newline at end of file diff --git a/src/main/resources/templates/fragment/header.html b/src/main/resources/templates/fragment/header.html index 10c715c..2c3772f 100644 --- a/src/main/resources/templates/fragment/header.html +++ b/src/main/resources/templates/fragment/header.html @@ -21,6 +21,9 @@ + diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index f3d9d9d..cce819b 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -6,15 +6,19 @@ >
-

Welcome!

+
+
+

Welcome to Coyote Credit

-

Your Accounts:

- +

+ A simulated asset trading platform developed for building a stronger understanding of investment and wealth management. +

+ +
+ +

+ You can visit the Exchanges page to view a list of exchanges that you're participating in. +

+
+
\ No newline at end of file diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html deleted file mode 100644 index 79a0052..0000000 --- a/src/main/resources/templates/login.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - CC - Login - - - - - - - - - - - -
- - - \ No newline at end of file diff --git a/src/main/resources/templates/public/login.html b/src/main/resources/templates/public/login.html new file mode 100644 index 0000000..f2ad304 --- /dev/null +++ b/src/main/resources/templates/public/login.html @@ -0,0 +1,29 @@ + + +
+
+
+ + +

Login

+
+
+ + +
+ +
+ + +
+ + + Register +
+
+
+
\ No newline at end of file diff --git a/src/main/resources/templates/public/register.html b/src/main/resources/templates/public/register.html new file mode 100644 index 0000000..039c738 --- /dev/null +++ b/src/main/resources/templates/public/register.html @@ -0,0 +1,32 @@ + + +
+
+
+

Register

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+ +
diff --git a/src/main/resources/templates/user.html b/src/main/resources/templates/user.html index 3bab167..e471d5f 100644 --- a/src/main/resources/templates/user.html +++ b/src/main/resources/templates/user.html @@ -13,6 +13,10 @@ Username + + Email + +