Added transfer, more improvements.
This commit is contained in:
		
							parent
							
								
									0538334df4
								
							
						
					
					
						commit
						6183ccbea9
					
				| 
						 | 
					@ -31,3 +31,5 @@ build/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### VS Code ###
 | 
					### VS Code ###
 | 
				
			||||||
.vscode/
 | 
					.vscode/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/config/
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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 {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -26,7 +26,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 | 
				
			||||||
		http
 | 
							http
 | 
				
			||||||
			.authorizeRequests()
 | 
								.authorizeRequests()
 | 
				
			||||||
				.antMatchers(
 | 
									.antMatchers(
 | 
				
			||||||
						"/login", "/login/processing", "/static/**"
 | 
											"/login*", "/login/processing", "/register*", "/activate*", "/static/**"
 | 
				
			||||||
				).permitAll()
 | 
									).permitAll()
 | 
				
			||||||
				.and()
 | 
									.and()
 | 
				
			||||||
			.authorizeRequests().anyRequest().authenticated()
 | 
								.authorizeRequests().anyRequest().authenticated()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
package nl.andrewl.coyotecredit.ctl;
 | 
					package nl.andrewl.coyotecredit.ctl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
 | 
					 | 
				
			||||||
import lombok.RequiredArgsConstructor;
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.ctl.dto.TransferPayload;
 | 
				
			||||||
import nl.andrewl.coyotecredit.model.User;
 | 
					import nl.andrewl.coyotecredit.model.User;
 | 
				
			||||||
import nl.andrewl.coyotecredit.service.AccountService;
 | 
					import nl.andrewl.coyotecredit.service.AccountService;
 | 
				
			||||||
import org.springframework.http.HttpStatus;
 | 
					import org.springframework.http.HttpStatus;
 | 
				
			||||||
| 
						 | 
					@ -12,8 +12,6 @@ import org.springframework.util.MultiValueMap;
 | 
				
			||||||
import org.springframework.web.bind.annotation.*;
 | 
					import org.springframework.web.bind.annotation.*;
 | 
				
			||||||
import org.springframework.web.server.ResponseStatusException;
 | 
					import org.springframework.web.server.ResponseStatusException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.util.Map;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@Controller
 | 
					@Controller
 | 
				
			||||||
@RequestMapping(path = "/accounts/{accountId}")
 | 
					@RequestMapping(path = "/accounts/{accountId}")
 | 
				
			||||||
@RequiredArgsConstructor
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
| 
						 | 
					@ -44,4 +42,16 @@ public class AccountPage {
 | 
				
			||||||
		accountService.editBalances(accountId, user, paramMap);
 | 
							accountService.editBalances(accountId, user, paramMap);
 | 
				
			||||||
		return "redirect:/accounts/" + accountId;
 | 
							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;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,42 +10,48 @@ import org.springframework.ui.Model;
 | 
				
			||||||
import org.springframework.web.bind.annotation.*;
 | 
					import org.springframework.web.bind.annotation.*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Controller
 | 
					@Controller
 | 
				
			||||||
@RequestMapping(path = "/exchanges/{exchangeId}")
 | 
					@RequestMapping(path = "/exchanges")
 | 
				
			||||||
@RequiredArgsConstructor
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
public class ExchangePage {
 | 
					public class ExchangeController {
 | 
				
			||||||
	private final ExchangeService exchangeService;
 | 
						private final ExchangeService exchangeService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@GetMapping
 | 
						@GetMapping
 | 
				
			||||||
	public String get(Model model, @PathVariable long exchangeId, @AuthenticationPrincipal User user) {
 | 
						public String getExchanges(Model model, @AuthenticationPrincipal User user) {
 | 
				
			||||||
		model.addAttribute("exchange", exchangeService.getData(exchangeId, user));
 | 
							model.addAttribute("exchangeData", exchangeService.getExchanges(user));
 | 
				
			||||||
		return "exchange";
 | 
							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) {
 | 
						public String getAccountsPage(Model model, @PathVariable long exchangeId, @AuthenticationPrincipal User user) {
 | 
				
			||||||
		model.addAttribute("accounts", exchangeService.getAccounts(exchangeId, user));
 | 
							model.addAttribute("accounts", exchangeService.getAccounts(exchangeId, user));
 | 
				
			||||||
		return "exchange/accounts";
 | 
							return "exchange/accounts";
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@GetMapping(path = "/addAccount")
 | 
						@GetMapping(path = "/{exchangeId}/addAccount")
 | 
				
			||||||
	public String getAddAccountPage(@PathVariable long exchangeId, @AuthenticationPrincipal User user) {
 | 
						public String getAddAccountPage(@PathVariable long exchangeId, @AuthenticationPrincipal User user) {
 | 
				
			||||||
		exchangeService.ensureAdminAccount(exchangeId, user);
 | 
							exchangeService.ensureAdminAccount(exchangeId, user);
 | 
				
			||||||
		return "exchange/addAccount";
 | 
							return "exchange/addAccount";
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@PostMapping(path = "/addAccount")
 | 
						@PostMapping(path = "/{exchangeId}/addAccount")
 | 
				
			||||||
	public String postAddAcount(@PathVariable long exchangeId, @AuthenticationPrincipal User user, @ModelAttribute AddAccountPayload payload) {
 | 
						public String postAddAcount(@PathVariable long exchangeId, @AuthenticationPrincipal User user, @ModelAttribute AddAccountPayload payload) {
 | 
				
			||||||
		long accountId = exchangeService.addAccount(exchangeId, user, payload);
 | 
							long accountId = exchangeService.addAccount(exchangeId, user, payload);
 | 
				
			||||||
		return "redirect:/accounts/" + accountId;
 | 
							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) {
 | 
						public String getRemoveAccountPage(@PathVariable long exchangeId, @PathVariable long accountId, @AuthenticationPrincipal User user) {
 | 
				
			||||||
		exchangeService.ensureAdminAccount(exchangeId, user);
 | 
							exchangeService.ensureAdminAccount(exchangeId, user);
 | 
				
			||||||
		return "exchange/removeAccount";
 | 
							return "exchange/removeAccount";
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@PostMapping(path = "/removeAccount/{accountId}")
 | 
						@PostMapping(path = "/{exchangeId}/removeAccount/{accountId}")
 | 
				
			||||||
	public String postRemoveAccount(@PathVariable long exchangeId, @PathVariable long accountId, @AuthenticationPrincipal User user) {
 | 
						public String postRemoveAccount(@PathVariable long exchangeId, @PathVariable long accountId, @AuthenticationPrincipal User user) {
 | 
				
			||||||
		exchangeService.removeAccount(exchangeId, accountId, user);
 | 
							exchangeService.removeAccount(exchangeId, accountId, user);
 | 
				
			||||||
		return "redirect:/exchanges/" + exchangeId + "/accounts";
 | 
							return "redirect:/exchanges/" + exchangeId + "/accounts";
 | 
				
			||||||
| 
						 | 
					@ -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";
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -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";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,7 @@ package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public record AddAccountPayload(
 | 
					public record AddAccountPayload(
 | 
				
			||||||
		String name,
 | 
							String name,
 | 
				
			||||||
 | 
							String email,
 | 
				
			||||||
		String username,
 | 
							String username,
 | 
				
			||||||
		String password
 | 
							String password
 | 
				
			||||||
) {}
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,5 +3,6 @@ package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
public record BalanceData(
 | 
					public record BalanceData(
 | 
				
			||||||
	long id,
 | 
						long id,
 | 
				
			||||||
	String symbol,
 | 
						String symbol,
 | 
				
			||||||
 | 
						String type,
 | 
				
			||||||
	String amount
 | 
						String amount
 | 
				
			||||||
) {}
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record ExchangeAccountData(
 | 
				
			||||||
 | 
							ExchangeData exchange,
 | 
				
			||||||
 | 
							SimpleAccountData account
 | 
				
			||||||
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,7 @@ public record FullExchangeData (
 | 
				
			||||||
		String name,
 | 
							String name,
 | 
				
			||||||
		TradeableData primaryTradeable,
 | 
							TradeableData primaryTradeable,
 | 
				
			||||||
		List<TradeableData> supportedTradeables,
 | 
							List<TradeableData> supportedTradeables,
 | 
				
			||||||
		// Account info
 | 
							// Account info that's needed for determining if it's possible to do some actions.
 | 
				
			||||||
		boolean accountAdmin
 | 
							boolean accountAdmin,
 | 
				
			||||||
) {
 | 
							long accountId
 | 
				
			||||||
}
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record RegisterPayload (
 | 
				
			||||||
 | 
							String username,
 | 
				
			||||||
 | 
							String email,
 | 
				
			||||||
 | 
							String password
 | 
				
			||||||
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					@ -2,20 +2,26 @@ package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import nl.andrewl.coyotecredit.model.Tradeable;
 | 
					import nl.andrewl.coyotecredit.model.Tradeable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.text.DecimalFormat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public record TradeableData(
 | 
					public record TradeableData(
 | 
				
			||||||
		long id,
 | 
							long id,
 | 
				
			||||||
		String symbol,
 | 
							String symbol,
 | 
				
			||||||
		String type,
 | 
							String type,
 | 
				
			||||||
		String marketPriceUsd,
 | 
							String marketPriceUsd,
 | 
				
			||||||
 | 
							String formattedPriceUsd,
 | 
				
			||||||
		String name,
 | 
							String name,
 | 
				
			||||||
		String description
 | 
							String description
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
 | 
						public static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,##0.00");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public TradeableData(Tradeable t) {
 | 
						public TradeableData(Tradeable t) {
 | 
				
			||||||
		this(
 | 
							this(
 | 
				
			||||||
				t.getId(),
 | 
									t.getId(),
 | 
				
			||||||
				t.getSymbol(),
 | 
									t.getSymbol(),
 | 
				
			||||||
				t.getType().name(),
 | 
									t.getType().name(),
 | 
				
			||||||
				t.getMarketPriceUsd().toPlainString(),
 | 
									t.getMarketPriceUsd().toPlainString(),
 | 
				
			||||||
 | 
									DECIMAL_FORMAT.format(t.getMarketPriceUsd()),
 | 
				
			||||||
				t.getName(),
 | 
									t.getName(),
 | 
				
			||||||
				t.getDescription()
 | 
									t.getDescription()
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record TransferPayload (
 | 
				
			||||||
 | 
							String recipientNumber,
 | 
				
			||||||
 | 
							String amount,
 | 
				
			||||||
 | 
							long tradeableId,
 | 
				
			||||||
 | 
							String message
 | 
				
			||||||
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					@ -2,5 +2,6 @@ package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public record UserData (
 | 
					public record UserData (
 | 
				
			||||||
		long id,
 | 
							long id,
 | 
				
			||||||
		String username
 | 
							String username,
 | 
				
			||||||
 | 
							String email
 | 
				
			||||||
) {}
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,9 @@ import nl.andrewl.coyotecredit.model.Tradeable;
 | 
				
			||||||
import org.springframework.data.jpa.repository.JpaRepository;
 | 
					import org.springframework.data.jpa.repository.JpaRepository;
 | 
				
			||||||
import org.springframework.stereotype.Repository;
 | 
					import org.springframework.stereotype.Repository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Repository
 | 
					@Repository
 | 
				
			||||||
public interface TradeableRepository extends JpaRepository<Tradeable, Long> {
 | 
					public interface TradeableRepository extends JpaRepository<Tradeable, Long> {
 | 
				
			||||||
 | 
						List<Tradeable> findAllByExchangeNull();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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<Transfer, Long> {
 | 
				
			||||||
 | 
						@Query(
 | 
				
			||||||
 | 
								"SELECT t FROM Transfer t " +
 | 
				
			||||||
 | 
								"WHERE t.senderNumber = :accountNumber OR t.recipientNumber = :accountNumber " +
 | 
				
			||||||
 | 
								"ORDER BY t.timestamp DESC"
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						Page<Transfer> findAllForAccount(String accountNumber, Pageable pageable);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -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<UserActivationToken, String> {
 | 
				
			||||||
 | 
						void deleteAllByExpiresAtBefore(LocalDateTime time);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -9,4 +9,5 @@ import java.util.Optional;
 | 
				
			||||||
@Repository
 | 
					@Repository
 | 
				
			||||||
public interface UserRepository extends JpaRepository<User, Long> {
 | 
					public interface UserRepository extends JpaRepository<User, Long> {
 | 
				
			||||||
	Optional<User> findByUsername(String username);
 | 
						Optional<User> findByUsername(String username);
 | 
				
			||||||
 | 
						boolean existsByUsername(String username);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -47,7 +47,7 @@ public class Exchange {
 | 
				
			||||||
	 * The set of custom tradeables created specifically for use in this exchange.
 | 
						 * The set of custom tradeables created specifically for use in this exchange.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	@OneToMany(mappedBy = "exchange", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
 | 
						@OneToMany(mappedBy = "exchange", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
 | 
				
			||||||
	private Set<CustomTradeable> customTradeables;
 | 
						private Set<Tradeable> customTradeables;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * The set of accounts that are registered with this exchange.
 | 
						 * The set of accounts that are registered with this exchange.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,7 @@ package nl.andrewl.coyotecredit.model;
 | 
				
			||||||
import lombok.AccessLevel;
 | 
					import lombok.AccessLevel;
 | 
				
			||||||
import lombok.Getter;
 | 
					import lombok.Getter;
 | 
				
			||||||
import lombok.NoArgsConstructor;
 | 
					import lombok.NoArgsConstructor;
 | 
				
			||||||
 | 
					import lombok.Setter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import javax.persistence.*;
 | 
					import javax.persistence.*;
 | 
				
			||||||
import java.math.BigDecimal;
 | 
					import java.math.BigDecimal;
 | 
				
			||||||
| 
						 | 
					@ -29,6 +30,7 @@ public class Tradeable {
 | 
				
			||||||
	private TradeableType type;
 | 
						private TradeableType type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Column(nullable = false, precision = 24, scale = 10)
 | 
						@Column(nullable = false, precision = 24, scale = 10)
 | 
				
			||||||
 | 
						@Setter
 | 
				
			||||||
	private BigDecimal marketPriceUsd = new BigDecimal(1);
 | 
						private BigDecimal marketPriceUsd = new BigDecimal(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Column(nullable = false)
 | 
						@Column(nullable = false)
 | 
				
			||||||
| 
						 | 
					@ -37,12 +39,19 @@ public class Tradeable {
 | 
				
			||||||
	@Column
 | 
						@Column
 | 
				
			||||||
	private String description;
 | 
						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.symbol = symbol;
 | 
				
			||||||
		this.type = type;
 | 
							this.type = type;
 | 
				
			||||||
		this.name = name;
 | 
							this.name = name;
 | 
				
			||||||
		this.description = description;
 | 
							this.description = description;
 | 
				
			||||||
		this.marketPriceUsd = marketPriceUsd;
 | 
							this.marketPriceUsd = marketPriceUsd;
 | 
				
			||||||
 | 
							this.exchange = exchange;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Override
 | 
						@Override
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -3,10 +3,13 @@ package nl.andrewl.coyotecredit.model;
 | 
				
			||||||
import lombok.AccessLevel;
 | 
					import lombok.AccessLevel;
 | 
				
			||||||
import lombok.Getter;
 | 
					import lombok.Getter;
 | 
				
			||||||
import lombok.NoArgsConstructor;
 | 
					import lombok.NoArgsConstructor;
 | 
				
			||||||
 | 
					import lombok.Setter;
 | 
				
			||||||
import org.springframework.security.core.GrantedAuthority;
 | 
					import org.springframework.security.core.GrantedAuthority;
 | 
				
			||||||
import org.springframework.security.core.userdetails.UserDetails;
 | 
					import org.springframework.security.core.userdetails.UserDetails;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import javax.persistence.*;
 | 
					import javax.persistence.*;
 | 
				
			||||||
 | 
					import java.time.LocalDateTime;
 | 
				
			||||||
 | 
					import java.time.ZoneOffset;
 | 
				
			||||||
import java.util.Collection;
 | 
					import java.util.Collection;
 | 
				
			||||||
import java.util.Collections;
 | 
					import java.util.Collections;
 | 
				
			||||||
import java.util.HashSet;
 | 
					import java.util.HashSet;
 | 
				
			||||||
| 
						 | 
					@ -29,15 +32,27 @@ public class User implements UserDetails {
 | 
				
			||||||
	@Column(nullable = false)
 | 
						@Column(nullable = false)
 | 
				
			||||||
	private String passwordHash;
 | 
						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.
 | 
						 * The set of accounts this user has.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	@OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
 | 
						@OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
 | 
				
			||||||
	private Set<Account> accounts;
 | 
						private Set<Account> accounts;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public User(String username, String passwordHash) {
 | 
						public User(String username, String passwordHash, String email) {
 | 
				
			||||||
		this.username = username;
 | 
							this.username = username;
 | 
				
			||||||
		this.passwordHash = passwordHash;
 | 
							this.passwordHash = passwordHash;
 | 
				
			||||||
 | 
							this.email = email;
 | 
				
			||||||
 | 
							this.createdAt = LocalDateTime.now(ZoneOffset.UTC);
 | 
				
			||||||
		this.accounts = new HashSet<>();
 | 
							this.accounts = new HashSet<>();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -75,6 +90,6 @@ public class User implements UserDetails {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Override
 | 
						@Override
 | 
				
			||||||
	public boolean isEnabled() {
 | 
						public boolean isEnabled() {
 | 
				
			||||||
		return true;
 | 
							return this.activated;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,17 +1,12 @@
 | 
				
			||||||
package nl.andrewl.coyotecredit.service;
 | 
					package nl.andrewl.coyotecredit.service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import lombok.RequiredArgsConstructor;
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
import nl.andrewl.coyotecredit.ctl.dto.BalanceData;
 | 
					import nl.andrewl.coyotecredit.ctl.dto.*;
 | 
				
			||||||
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.dao.AccountRepository;
 | 
					import nl.andrewl.coyotecredit.dao.AccountRepository;
 | 
				
			||||||
import nl.andrewl.coyotecredit.dao.TradeableRepository;
 | 
					import nl.andrewl.coyotecredit.dao.TradeableRepository;
 | 
				
			||||||
import nl.andrewl.coyotecredit.dao.TransactionRepository;
 | 
					import nl.andrewl.coyotecredit.dao.TransactionRepository;
 | 
				
			||||||
import nl.andrewl.coyotecredit.model.Account;
 | 
					import nl.andrewl.coyotecredit.dao.TransferRepository;
 | 
				
			||||||
import nl.andrewl.coyotecredit.model.Balance;
 | 
					import nl.andrewl.coyotecredit.model.*;
 | 
				
			||||||
import nl.andrewl.coyotecredit.model.Tradeable;
 | 
					 | 
				
			||||||
import nl.andrewl.coyotecredit.model.User;
 | 
					 | 
				
			||||||
import org.springframework.data.domain.PageRequest;
 | 
					import org.springframework.data.domain.PageRequest;
 | 
				
			||||||
import org.springframework.http.HttpStatus;
 | 
					import org.springframework.http.HttpStatus;
 | 
				
			||||||
import org.springframework.stereotype.Service;
 | 
					import org.springframework.stereotype.Service;
 | 
				
			||||||
| 
						 | 
					@ -29,6 +24,61 @@ public class AccountService {
 | 
				
			||||||
	private final AccountRepository accountRepository;
 | 
						private final AccountRepository accountRepository;
 | 
				
			||||||
	private final TransactionRepository transactionRepository;
 | 
						private final TransactionRepository transactionRepository;
 | 
				
			||||||
	private final TradeableRepository tradeableRepository;
 | 
						private final TradeableRepository tradeableRepository;
 | 
				
			||||||
 | 
						private final TransferRepository transferRepository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Transactional(readOnly = true)
 | 
				
			||||||
 | 
						public List<BalanceData> 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 (
 | 
						public static record AccountData (
 | 
				
			||||||
			long id,
 | 
								long id,
 | 
				
			||||||
| 
						 | 
					@ -71,10 +121,15 @@ public class AccountService {
 | 
				
			||||||
						account.getExchange().getPrimaryTradeable().getSymbol()
 | 
											account.getExchange().getPrimaryTradeable().getSymbol()
 | 
				
			||||||
				),
 | 
									),
 | 
				
			||||||
				account.getBalances().stream()
 | 
									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))
 | 
											.sorted(Comparator.comparing(BalanceData::symbol))
 | 
				
			||||||
						.toList(),
 | 
											.toList(),
 | 
				
			||||||
				account.getTotalBalance().toPlainString(),
 | 
									TradeableData.DECIMAL_FORMAT.format(account.getTotalBalance()),
 | 
				
			||||||
				transactionData
 | 
									transactionData
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,7 +45,8 @@ public class ExchangeService {
 | 
				
			||||||
						.map(TradeableData::new)
 | 
											.map(TradeableData::new)
 | 
				
			||||||
						.sorted(Comparator.comparing(TradeableData::symbol))
 | 
											.sorted(Comparator.comparing(TradeableData::symbol))
 | 
				
			||||||
						.toList(),
 | 
											.toList(),
 | 
				
			||||||
				account.isAdmin()
 | 
									account.isAdmin(),
 | 
				
			||||||
 | 
									account.getId()
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -91,7 +92,7 @@ public class ExchangeService {
 | 
				
			||||||
		if (!account.isAdmin()) {
 | 
							if (!account.isAdmin()) {
 | 
				
			||||||
			throw new ResponseStatusException(HttpStatus.NOT_FOUND);
 | 
								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(
 | 
							Account a = accountRepository.save(new Account(
 | 
				
			||||||
				AccountNumberUtils.generate(),
 | 
									AccountNumberUtils.generate(),
 | 
				
			||||||
				u,
 | 
									u,
 | 
				
			||||||
| 
						 | 
					@ -188,4 +189,15 @@ public class ExchangeService {
 | 
				
			||||||
		Transaction tx = new Transaction(account.getNumber(), exchange, from, fromValue, to, toValue, LocalDateTime.now(ZoneOffset.UTC));
 | 
							Transaction tx = new Transaction(account.getNumber(), exchange, from, fromValue, to, toValue, LocalDateTime.now(ZoneOffset.UTC));
 | 
				
			||||||
		transactionRepository.save(tx);
 | 
							transactionRepository.save(tx);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Transactional(readOnly = true)
 | 
				
			||||||
 | 
						public List<ExchangeAccountData> 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();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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<Tradeable> 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<byte[]> 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<byte[]> 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<byte[]> 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;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,20 +1,40 @@
 | 
				
			||||||
package nl.andrewl.coyotecredit.service;
 | 
					package nl.andrewl.coyotecredit.service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import lombok.RequiredArgsConstructor;
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.ctl.dto.RegisterPayload;
 | 
				
			||||||
import nl.andrewl.coyotecredit.ctl.dto.UserData;
 | 
					import nl.andrewl.coyotecredit.ctl.dto.UserData;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.dao.UserActivationTokenRepository;
 | 
				
			||||||
import nl.andrewl.coyotecredit.dao.UserRepository;
 | 
					import nl.andrewl.coyotecredit.dao.UserRepository;
 | 
				
			||||||
import nl.andrewl.coyotecredit.model.User;
 | 
					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.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.stereotype.Service;
 | 
				
			||||||
import org.springframework.transaction.annotation.Transactional;
 | 
					import org.springframework.transaction.annotation.Transactional;
 | 
				
			||||||
import org.springframework.web.server.ResponseStatusException;
 | 
					import org.springframework.web.server.ResponseStatusException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javax.mail.MessagingException;
 | 
				
			||||||
 | 
					import javax.mail.internet.MimeMessage;
 | 
				
			||||||
 | 
					import java.time.LocalDateTime;
 | 
				
			||||||
 | 
					import java.time.ZoneOffset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Service
 | 
					@Service
 | 
				
			||||||
@RequiredArgsConstructor
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
public class UserService {
 | 
					public class UserService {
 | 
				
			||||||
	private final UserRepository userRepository;
 | 
						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) {
 | 
						public UserData getUser(long userId, User requestingUser) {
 | 
				
			||||||
		User user;
 | 
							User user;
 | 
				
			||||||
		if (requestingUser.getId().equals(userId)) {
 | 
							if (requestingUser.getId().equals(userId)) {
 | 
				
			||||||
| 
						 | 
					@ -22,6 +42,62 @@ public class UserService {
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			user = userRepository.findById(userId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
								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 <noreply@coyote-credit.com>");
 | 
				
			||||||
 | 
							helper.setTo(token.getUser().getEmail());
 | 
				
			||||||
 | 
							helper.setSubject("Activate Your Account");
 | 
				
			||||||
 | 
							String activationUrl = baseUrl + "/activate?token=" + token.getToken();
 | 
				
			||||||
 | 
							helper.setText(String.format(
 | 
				
			||||||
 | 
									"""
 | 
				
			||||||
 | 
									<p>In order to complete your account registration for Coyote Credit,
 | 
				
			||||||
 | 
									please follow this link:</p>
 | 
				
			||||||
 | 
									<a href="%s">%s</a>.
 | 
				
			||||||
 | 
									<p>Note that this link will expire in 24 hours.</p>
 | 
				
			||||||
 | 
									<p>If you did not register for an account, or you are unaware of
 | 
				
			||||||
 | 
									someone registering on your behalf, you may safely ignore this
 | 
				
			||||||
 | 
									email.</p>""",
 | 
				
			||||||
 | 
									activationUrl, activationUrl
 | 
				
			||||||
 | 
							), true);
 | 
				
			||||||
 | 
							mailSender.send(msg);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Scheduled(cron = "@midnight")
 | 
				
			||||||
 | 
						public void removeExpiredActivationTokens() {
 | 
				
			||||||
 | 
							activationTokenRepository.deleteAllByExpiresAtBefore(LocalDateTime.now(ZoneOffset.UTC));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -4,3 +4,8 @@ spring.datasource.password=tester
 | 
				
			||||||
 | 
					
 | 
				
			||||||
spring.jpa.hibernate.ddl-auto=update
 | 
					spring.jpa.hibernate.ddl-auto=update
 | 
				
			||||||
spring.jpa.open-in-view=false
 | 
					spring.jpa.open-in-view=false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					spring.mail.host=127.0.0.1
 | 
				
			||||||
 | 
					spring.mail.port=1025
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					coyote-credit.base-url=http://localhost:8080
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| 
						 | 
					@ -8,25 +8,32 @@
 | 
				
			||||||
<div id="content" class="container">
 | 
					<div id="content" class="container">
 | 
				
			||||||
    <h1>Account <span th:text="${account.number()}"></span></h1>
 | 
					    <h1>Account <span th:text="${account.number()}"></span></h1>
 | 
				
			||||||
    <p>In <a th:href="@{/exchanges/{id}(id=${account.exchange().id()})}" th:text="${account.exchange().name()}"></a></p>
 | 
					    <p>In <a th:href="@{/exchanges/{id}(id=${account.exchange().id()})}" th:text="${account.exchange().name()}"></a></p>
 | 
				
			||||||
    <p>Total value of <span th:text="${account.totalBalance() + ' ' + account.exchange().primaryTradeable()}"></span></p>
 | 
					    <p>Total value of <span class="currency" th:text="${account.totalBalance()}"></span> <span th:text="${account.exchange().primaryTradeable()}"></span></p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <h3>Overview</h3>
 | 
					    <h3>Overview</h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <table class="table">
 | 
					    <table class="table">
 | 
				
			||||||
        <thead>
 | 
					        <thead>
 | 
				
			||||||
            <tr>
 | 
					            <tr>
 | 
				
			||||||
                <th>Currency</th>
 | 
					                <th>Asset</th>
 | 
				
			||||||
 | 
					                <th>Type</th>
 | 
				
			||||||
                <th>Balance</th>
 | 
					                <th>Balance</th>
 | 
				
			||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
        </thead>
 | 
					        </thead>
 | 
				
			||||||
        <tbody>
 | 
					        <tbody>
 | 
				
			||||||
            <tr th:each="bal : ${account.balances()}">
 | 
					            <tr th:each="bal : ${account.balances()}">
 | 
				
			||||||
                <td th:text="${bal.symbol()}"></td>
 | 
					                <td th:text="${bal.symbol()}"></td>
 | 
				
			||||||
                <td th:text="${bal.amount()}"></td>
 | 
					                <td th:text="${bal.type()}"></td>
 | 
				
			||||||
 | 
					                <td class="currency" th:text="${bal.amount()}"></td>
 | 
				
			||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
        </tbody>
 | 
					        </tbody>
 | 
				
			||||||
    </table>
 | 
					    </table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <a class="btn btn-success" th:href="@{/trade/{account}(account=${account.id()})}">Trade</a>
 | 
				
			||||||
 | 
					    <a class="btn btn-success" th:href="@{/accounts/{aId}/transfer(aId=${account.id()})}">Transfer</a>
 | 
				
			||||||
 | 
					    <a class="btn btn-primary" th:if="${account.userAdmin()}" th:href="@{/accounts/{aId}/editBalances(aId=${account.id()})}">Edit Balances</a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div th:if="${!account.recentTransactions().isEmpty()}">
 | 
				
			||||||
        <h3>Recent Transactions</h3>
 | 
					        <h3>Recent Transactions</h3>
 | 
				
			||||||
        <table class="table">
 | 
					        <table class="table">
 | 
				
			||||||
            <thead>
 | 
					            <thead>
 | 
				
			||||||
| 
						 | 
					@ -41,14 +48,12 @@
 | 
				
			||||||
            <tbody>
 | 
					            <tbody>
 | 
				
			||||||
            <tr th:each="tx : ${account.recentTransactions()}">
 | 
					            <tr th:each="tx : ${account.recentTransactions()}">
 | 
				
			||||||
                <td th:text="${tx.from().name()}"></td>
 | 
					                <td th:text="${tx.from().name()}"></td>
 | 
				
			||||||
                <td th:text="${tx.fromAmount()}"></td>
 | 
					                <td class="currency" th:text="${tx.fromAmount()}"></td>
 | 
				
			||||||
                <td th:text="${tx.to().name()}"></td>
 | 
					                <td th:text="${tx.to().name()}"></td>
 | 
				
			||||||
                <td th:text="${tx.toAmount()}"></td>
 | 
					                <td class="currency" th:text="${tx.toAmount()}"></td>
 | 
				
			||||||
                <td th:text="${tx.timestamp()}"></td>
 | 
					                <td th:text="${tx.timestamp()}"></td>
 | 
				
			||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
            </tbody>
 | 
					            </tbody>
 | 
				
			||||||
        </table>
 | 
					        </table>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
    <a class="btn btn-success" th:href="@{/trade/{account}(account=${account.id()})}">Trade</a>
 | 
					 | 
				
			||||||
    <a class="btn btn-primary" th:if="${account.userAdmin()}" th:href="@{/accounts/{aId}/editBalances(aId=${account.id()})}">Edit Balances</a>
 | 
					 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,7 @@
 | 
				
			||||||
    <form th:action="@{/accounts/{aId}/editBalances(aId=${accountId})}" th:method="post">
 | 
					    <form th:action="@{/accounts/{aId}/editBalances(aId=${accountId})}" th:method="post">
 | 
				
			||||||
        <div class="mb-3" th:each="bal, iter : ${account.balances()}">
 | 
					        <div class="mb-3" th:each="bal, iter : ${account.balances()}">
 | 
				
			||||||
            <label th:for="${'tradeable-' + bal.id()}" class="form-label" th:text="${bal.symbol()}"></label>
 | 
					            <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"/>
 | 
					            <input type="number" min="0" th:value="${bal.amount()}" th:name="${'tradeable-' + bal.id()}" class="form-control" required/>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <button type="submit" class="btn btn-success">Submit</button>
 | 
					        <button type="submit" class="btn btn-success">Submit</button>
 | 
				
			||||||
    </form>
 | 
					    </form>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html
 | 
				
			||||||
 | 
					        lang="en"
 | 
				
			||||||
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
 | 
					        th:replace="~{layout/basic_page :: layout (title='Add Account', content=~{::#content})}"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					<div id="content" class="container">
 | 
				
			||||||
 | 
					    <form th:action="@{/accounts/{aId}/transfer(aId=${accountId})}" th:method="post">
 | 
				
			||||||
 | 
					        <div class="mb-3">
 | 
				
			||||||
 | 
					            <label for="recipientNumberInput" class="form-label">Recipient Account Number</label>
 | 
				
			||||||
 | 
					            <input type="text" name="recipientNumber" class="form-control" id="recipientNumberInput" required/>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="mb-3">
 | 
				
			||||||
 | 
					            <label for="tradeableSelect" class="form-label">Asset</label>
 | 
				
			||||||
 | 
					            <select class="form-select" id="tradeableSelect" name="tradeableId" required>
 | 
				
			||||||
 | 
					                <option
 | 
				
			||||||
 | 
					                        th:each="b : ${balances}"
 | 
				
			||||||
 | 
					                        th:text="${b.symbol() + ' - Balance ' + b.amount()}"
 | 
				
			||||||
 | 
					                        th:value="${b.id()}"
 | 
				
			||||||
 | 
					                ></option>
 | 
				
			||||||
 | 
					            </select>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="mb-3">
 | 
				
			||||||
 | 
					            <label for="amountInput" class="form-label">Amount</label>
 | 
				
			||||||
 | 
					            <input type="number" min="0" name="amount" class="form-control" id="amountInput" required/>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="mb-3">
 | 
				
			||||||
 | 
					            <label for="messageTextArea" class="form-label">Message</label>
 | 
				
			||||||
 | 
					            <textarea class="form-control" name="message" rows="3" id="messageTextArea"></textarea>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <button type="submit" class="btn btn-success">Submit</button>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -7,8 +7,6 @@
 | 
				
			||||||
<div id="content" class="container">
 | 
					<div id="content" class="container">
 | 
				
			||||||
    <h1>Accounts</h1>
 | 
					    <h1>Accounts</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <a class="btn btn-primary" th:href="@{/exchanges/{eId}/addAccount(eId=${exchangeId})}">Add Account</a>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <table class="table">
 | 
					    <table class="table">
 | 
				
			||||||
        <thead>
 | 
					        <thead>
 | 
				
			||||||
            <tr>
 | 
					            <tr>
 | 
				
			||||||
| 
						 | 
					@ -23,9 +21,11 @@
 | 
				
			||||||
                <td><a th:href="@{/accounts/{id}(id=${account.id()})}" th:text="${account.number()}"></a></td>
 | 
					                <td><a th:href="@{/accounts/{id}(id=${account.id()})}" th:text="${account.number()}"></a></td>
 | 
				
			||||||
                <td th:text="${account.name()}"></td>
 | 
					                <td th:text="${account.name()}"></td>
 | 
				
			||||||
                <td th:text="${account.admin()}"></td>
 | 
					                <td th:text="${account.admin()}"></td>
 | 
				
			||||||
                <td th:text="${account.totalBalance()}"></td>
 | 
					                <td class="currency" th:text="${account.totalBalance()}"></td>
 | 
				
			||||||
                <td><a th:href="@{/exchanges/{eId}/removeAccount/{aId}(eId=${exchangeId}, aId=${account.id()})}">Remove</a></td>
 | 
					                <td><a th:href="@{/exchanges/{eId}/removeAccount/{aId}(eId=${exchangeId}, aId=${account.id()})}">Remove</a></td>
 | 
				
			||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
        </tbody>
 | 
					        </tbody>
 | 
				
			||||||
    </table>
 | 
					    </table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <a class="btn btn-primary" th:href="@{/exchanges/{eId}/addAccount(eId=${exchangeId})}">Add Account</a>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,15 +13,19 @@
 | 
				
			||||||
    <form th:href="@{/exchanges/{eId}/addAccount(eId=${exchangeId})}" th:method="post">
 | 
					    <form th:href="@{/exchanges/{eId}/addAccount(eId=${exchangeId})}" th:method="post">
 | 
				
			||||||
        <div class="mb-3">
 | 
					        <div class="mb-3">
 | 
				
			||||||
            <label for="nameInput" class="form-label">Name</label>
 | 
					            <label for="nameInput" class="form-label">Name</label>
 | 
				
			||||||
            <input id="nameInput" type="text" class="form-control" name="name"/>
 | 
					            <input id="nameInput" type="text" class="form-control" name="name" required/>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="mb-3">
 | 
					        <div class="mb-3">
 | 
				
			||||||
            <label for="usernameInput" class="form-label">Username</label>
 | 
					            <label for="usernameInput" class="form-label">Username</label>
 | 
				
			||||||
            <input id="usernameInput" type="text" class="form-control" name="username"/>
 | 
					            <input id="usernameInput" type="text" class="form-control" name="username" required/>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="mb-3">
 | 
				
			||||||
 | 
					            <label for="emailInput" class="form-label">Email</label>
 | 
				
			||||||
 | 
					            <input id="emailInput" type="email" class="form-control" name="email" required/>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="mb-3">
 | 
					        <div class="mb-3">
 | 
				
			||||||
            <label for="passwordInput" class="form-label">Password</label>
 | 
					            <label for="passwordInput" class="form-label">Password</label>
 | 
				
			||||||
            <input id="passwordInput" type="password" class="form-control" name="password"/>
 | 
					            <input id="passwordInput" type="password" class="form-control" name="password" required/>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <input type="hidden" name="_csrf" th:value="${_csrf.getToken()}"/>
 | 
					        <input type="hidden" name="_csrf" th:value="${_csrf.getToken()}"/>
 | 
				
			||||||
        <button type="submit" class="btn btn-primary">Submit</button>
 | 
					        <button type="submit" class="btn btn-primary">Submit</button>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,16 +8,16 @@
 | 
				
			||||||
    <h1 th:text="${exchange.name()}"></h1>
 | 
					    <h1 th:text="${exchange.name()}"></h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <p>
 | 
					    <p>
 | 
				
			||||||
        Primary tradeable: <span th:text="${exchange.primaryTradeable().name()}"></span>
 | 
					        Primary asset: <span th:text="${exchange.primaryTradeable().name()}"></span>
 | 
				
			||||||
    </p>
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <h3>Supported Tradeable Currencies / Stocks</h3>
 | 
					    <h3>Tradeable Assets</h3>
 | 
				
			||||||
    <table class="table">
 | 
					    <table class="table">
 | 
				
			||||||
        <thead>
 | 
					        <thead>
 | 
				
			||||||
            <tr>
 | 
					            <tr>
 | 
				
			||||||
                <th>Symbol</th>
 | 
					                <th>Symbol</th>
 | 
				
			||||||
                <th>Type</th>
 | 
					                <th>Type</th>
 | 
				
			||||||
                <th>Price ($)</th>
 | 
					                <th>Price (in USD)</th>
 | 
				
			||||||
                <th>Name</th>
 | 
					                <th>Name</th>
 | 
				
			||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
        </thead>
 | 
					        </thead>
 | 
				
			||||||
| 
						 | 
					@ -25,15 +25,14 @@
 | 
				
			||||||
            <tr th:each="tradeable : ${exchange.supportedTradeables()}">
 | 
					            <tr th:each="tradeable : ${exchange.supportedTradeables()}">
 | 
				
			||||||
                <td th:text="${tradeable.symbol()}"></td>
 | 
					                <td th:text="${tradeable.symbol()}"></td>
 | 
				
			||||||
                <td th:text="${tradeable.type()}"></td>
 | 
					                <td th:text="${tradeable.type()}"></td>
 | 
				
			||||||
                <td th:text="${tradeable.marketPriceUsd()}"></td>
 | 
					                <td class="currency" th:text="${tradeable.formattedPriceUsd()}"></td>
 | 
				
			||||||
                <td th:text="${tradeable.name()}"></td>
 | 
					                <td th:text="${tradeable.name()}"></td>
 | 
				
			||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
        </tbody>
 | 
					        </tbody>
 | 
				
			||||||
    </table>
 | 
					    </table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div th:if="${exchange.accountAdmin()}">
 | 
					 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
            <a class="btn btn-primary" th:href="@{/exchanges/{eId}/accounts(eId=${exchange.id()})}">View Accounts</a>
 | 
					        <a class="btn btn-primary" th:href="@{/accounts/{aId}(aId=${exchange.accountId()})}">My Account</a>
 | 
				
			||||||
        </div>
 | 
					        <a class="btn btn-primary" th:if="${exchange.accountAdmin()}" th:href="@{/exchanges/{eId}/accounts(eId=${exchange.id()})}">All Accounts</a>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,34 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html
 | 
				
			||||||
 | 
					        lang="en"
 | 
				
			||||||
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
 | 
					        th:replace="~{layout/basic_page :: layout (title='Exchanges', content=~{::#content})}"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					<div id="content" class="container">
 | 
				
			||||||
 | 
					    <h1>Exchanges</h1>
 | 
				
			||||||
 | 
					    <table class="table">
 | 
				
			||||||
 | 
					        <thead>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					                <th>Name</th>
 | 
				
			||||||
 | 
					                <th>Primary Asset</th>
 | 
				
			||||||
 | 
					                <th>Account</th>
 | 
				
			||||||
 | 
					                <th>Estimated Balance</th>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					        </thead>
 | 
				
			||||||
 | 
					        <tbody>
 | 
				
			||||||
 | 
					            <tr th:each="ed : ${exchangeData}">
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    <a th:text="${ed.exchange().name()}" th:href="@{/exchanges/{eId}(eId=${ed.exchange().id()})}"></a>
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td th:text="${ed.exchange().primaryTradeable()}"></td>
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    <a th:text="${ed.account().number()}" th:href="@{/accounts/{aId}(aId=${ed.account().id()})}"></a>
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td class="currency" th:text="${ed.account().totalBalance()}"></td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					        </tbody>
 | 
				
			||||||
 | 
					    </table>
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					        Use this page to view a list of all exchanges you're participating in. Click on the <strong>name</strong> of the exchange to view its page, and click on your <strong>account</strong> number to view your account information for a given exchange. The <strong>estimated balance</strong> 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.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -21,6 +21,9 @@
 | 
				
			||||||
                <li class="nav-item">
 | 
					                <li class="nav-item">
 | 
				
			||||||
                    <a class="nav-link" th:href="@{/}">Home</a>
 | 
					                    <a class="nav-link" th:href="@{/}">Home</a>
 | 
				
			||||||
                </li>
 | 
					                </li>
 | 
				
			||||||
 | 
					                <li class="nav-item">
 | 
				
			||||||
 | 
					                    <a class="nav-link" th:href="@{/exchanges}">Exchanges</a>
 | 
				
			||||||
 | 
					                </li>
 | 
				
			||||||
                <li class="nav-item">
 | 
					                <li class="nav-item">
 | 
				
			||||||
                    <a class="nav-link" th:href="@{/users/{id}(id=${#authentication.getPrincipal().getId()})}">My Profile</a>
 | 
					                    <a class="nav-link" th:href="@{/users/{id}(id=${#authentication.getPrincipal().getId()})}">My Profile</a>
 | 
				
			||||||
                </li>
 | 
					                </li>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,15 +6,19 @@
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div id="content" class="container">
 | 
					<div id="content" class="container">
 | 
				
			||||||
    <h1>Welcome!</h1>
 | 
					    <div class="row justify-content-center">
 | 
				
			||||||
 | 
					        <div class="col-lg-6">
 | 
				
			||||||
 | 
					            <h1 class="display-4">Welcome to Coyote Credit</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <h3>Your Accounts:</h3>
 | 
					            <p class="lead">
 | 
				
			||||||
    <ul>
 | 
					                A simulated asset trading platform developed for building a stronger understanding of investment and wealth management.
 | 
				
			||||||
        <li th:each="account : ${accounts}">
 | 
					            </p>
 | 
				
			||||||
            <a th:href="@{/accounts/{id}(id=${account.id()})}">
 | 
					
 | 
				
			||||||
                <span th:text="${account.accountNumber()}"></span> @
 | 
					            <hr>
 | 
				
			||||||
                <span th:text="${account.exchangeName()}"></span>
 | 
					
 | 
				
			||||||
            </a>
 | 
					            <p>
 | 
				
			||||||
        </li>
 | 
					                You can visit the <a th:href="@{/exchanges}">Exchanges</a> page to view a list of exchanges that you're participating in.
 | 
				
			||||||
    </ul>
 | 
					            </p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -1,23 +0,0 @@
 | 
				
			||||||
<!DOCTYPE html>
 | 
					 | 
				
			||||||
<html
 | 
					 | 
				
			||||||
        lang="en"
 | 
					 | 
				
			||||||
        xmlns:th="http://www.thymeleaf.org"
 | 
					 | 
				
			||||||
>
 | 
					 | 
				
			||||||
<head>
 | 
					 | 
				
			||||||
    <meta charset="UTF-8">
 | 
					 | 
				
			||||||
    <title>CC - Login</title>
 | 
					 | 
				
			||||||
</head>
 | 
					 | 
				
			||||||
<body>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<form th:action="@{/login/processing}" th:method="post">
 | 
					 | 
				
			||||||
    <label for="usernameInput">Username</label>
 | 
					 | 
				
			||||||
    <input name="username" type="text" id="usernameInput">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <label for="passwordInput">Password</label>
 | 
					 | 
				
			||||||
    <input name="password" type="password" id="passwordInput">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <input type="submit" value="Login">
 | 
					 | 
				
			||||||
</form>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</body>
 | 
					 | 
				
			||||||
</html>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,29 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html
 | 
				
			||||||
 | 
					        lang="en"
 | 
				
			||||||
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
 | 
					        th:replace="~{layout/basic_page :: layout (title='Login', content=~{::#content})}"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					<div id="content" class="container">
 | 
				
			||||||
 | 
					    <div class="row justify-content-center">
 | 
				
			||||||
 | 
					        <div class="col-lg-4">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <h1>Login</h1>
 | 
				
			||||||
 | 
					            <form th:action="@{/login/processing}" th:method="post">
 | 
				
			||||||
 | 
					                <div class="mb-3">
 | 
				
			||||||
 | 
					                    <label for="usernameInput" class="form-label">Username</label>
 | 
				
			||||||
 | 
					                    <input name="username" class="form-control" type="text" id="usernameInput" required>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="mb-3">
 | 
				
			||||||
 | 
					                    <label for="passwordInput" class="form-label">Password</label>
 | 
				
			||||||
 | 
					                    <input name="password" class="form-control" type="password" id="passwordInput" required>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <input type="submit" class="btn btn-primary" value="Login">
 | 
				
			||||||
 | 
					                <a class="btn btn-secondary" th:href="@{/register}">Register</a>
 | 
				
			||||||
 | 
					            </form>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html
 | 
				
			||||||
 | 
					        lang="en"
 | 
				
			||||||
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
 | 
					        th:replace="~{layout/basic_page :: layout (title='Register', content=~{::#content})}"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					<div id="content" class="container">
 | 
				
			||||||
 | 
					    <div class="row justify-content-center">
 | 
				
			||||||
 | 
					        <div class="col-lg-4">
 | 
				
			||||||
 | 
					            <h1>Register</h1>
 | 
				
			||||||
 | 
					            <form th:action="@{/register}" th:method="post">
 | 
				
			||||||
 | 
					                <div class="mb-3">
 | 
				
			||||||
 | 
					                    <label for="usernameInput" class="form-label">Username</label>
 | 
				
			||||||
 | 
					                    <input name="username" id="usernameInput" class="form-control" type="text" required/>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="mb-3">
 | 
				
			||||||
 | 
					                    <label for="emailInput" class="form-label">Email</label>
 | 
				
			||||||
 | 
					                    <input name="email" id="emailInput" class="form-control" type="email" required/>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="mb-3">
 | 
				
			||||||
 | 
					                    <label for="passwordInput" class="form-label">Password</label>
 | 
				
			||||||
 | 
					                    <input name="password" id="passwordInput" class="form-control" type="password" required/>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <input type="submit" class="btn btn-primary" value="Register">
 | 
				
			||||||
 | 
					            </form>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,10 @@
 | 
				
			||||||
                <th scope="row">Username</th>
 | 
					                <th scope="row">Username</th>
 | 
				
			||||||
                <td th:text="${user.username()}"></td>
 | 
					                <td th:text="${user.username()}"></td>
 | 
				
			||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					                <th scope="row">Email</th>
 | 
				
			||||||
 | 
					                <td th:text="${user.email()}"></td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
        </tbody>
 | 
					        </tbody>
 | 
				
			||||||
    </table>
 | 
					    </table>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue