Removed add account functionality in favor of invite user.
This commit is contained in:
		
							parent
							
								
									9b8a450234
								
							
						
					
					
						commit
						cff73d9803
					
				| 
						 | 
					@ -2,6 +2,8 @@ package nl.andrewl.coyotecredit.ctl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import lombok.RequiredArgsConstructor;
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
import nl.andrewl.coyotecredit.ctl.dto.AddAccountPayload;
 | 
					import nl.andrewl.coyotecredit.ctl.dto.AddAccountPayload;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.ctl.dto.EditExchangePayload;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.ctl.dto.InviteUserPayload;
 | 
				
			||||||
import nl.andrewl.coyotecredit.model.User;
 | 
					import nl.andrewl.coyotecredit.model.User;
 | 
				
			||||||
import nl.andrewl.coyotecredit.service.ExchangeService;
 | 
					import nl.andrewl.coyotecredit.service.ExchangeService;
 | 
				
			||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
 | 
					import org.springframework.security.core.annotation.AuthenticationPrincipal;
 | 
				
			||||||
| 
						 | 
					@ -9,6 +11,8 @@ import org.springframework.stereotype.Controller;
 | 
				
			||||||
import org.springframework.ui.Model;
 | 
					import org.springframework.ui.Model;
 | 
				
			||||||
import org.springframework.web.bind.annotation.*;
 | 
					import org.springframework.web.bind.annotation.*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javax.validation.Valid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Controller
 | 
					@Controller
 | 
				
			||||||
@RequestMapping(path = "/exchanges")
 | 
					@RequestMapping(path = "/exchanges")
 | 
				
			||||||
@RequiredArgsConstructor
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
| 
						 | 
					@ -33,16 +37,28 @@ public class ExchangeController {
 | 
				
			||||||
		return "exchange/accounts";
 | 
							return "exchange/accounts";
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@GetMapping(path = "/{exchangeId}/addAccount")
 | 
						@GetMapping(path = "/{exchangeId}/inviteUser")
 | 
				
			||||||
	public String getAddAccountPage(@PathVariable long exchangeId, @AuthenticationPrincipal User user) {
 | 
						public String getInviteUserPage(@PathVariable long exchangeId, @AuthenticationPrincipal User user) {
 | 
				
			||||||
		exchangeService.ensureAdminAccount(exchangeId, user);
 | 
							exchangeService.ensureAdminAccount(exchangeId, user);
 | 
				
			||||||
		return "exchange/addAccount";
 | 
							return "exchange/invite_user";
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@PostMapping(path = "/{exchangeId}/addAccount")
 | 
						@PostMapping(path = "/{exchangeId}/inviteUser")
 | 
				
			||||||
	public String postAddAcount(@PathVariable long exchangeId, @AuthenticationPrincipal User user, @ModelAttribute AddAccountPayload payload) {
 | 
						public String postInviteUser(@PathVariable long exchangeId, @AuthenticationPrincipal User user, @ModelAttribute InviteUserPayload payload) {
 | 
				
			||||||
		long accountId = exchangeService.addAccount(exchangeId, user, payload);
 | 
							exchangeService.inviteUser(exchangeId, user, payload);
 | 
				
			||||||
		return "redirect:/accounts/" + accountId;
 | 
							return "redirect:/exchanges/" + exchangeId;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@PostMapping(path = "/{exchangeId}/acceptInvite/{inviteId}")
 | 
				
			||||||
 | 
						public String postAcceptInvite(@PathVariable long exchangeId, @PathVariable long inviteId, @AuthenticationPrincipal User user) {
 | 
				
			||||||
 | 
							exchangeService.acceptInvite(exchangeId, inviteId, user);
 | 
				
			||||||
 | 
							return "redirect:/exchanges/" + exchangeId;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@PostMapping(path = "/{exchangeId}/rejectInvite/{inviteId}")
 | 
				
			||||||
 | 
						public String postRejectInvite(@PathVariable long exchangeId, @PathVariable long inviteId, @AuthenticationPrincipal User user) {
 | 
				
			||||||
 | 
							exchangeService.rejectInvite(exchangeId, inviteId, user);
 | 
				
			||||||
 | 
							return "redirect:/users/" + user.getId();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@GetMapping(path = "/{exchangeId}/removeAccount/{accountId}")
 | 
						@GetMapping(path = "/{exchangeId}/removeAccount/{accountId}")
 | 
				
			||||||
| 
						 | 
					@ -56,4 +72,16 @@ public class ExchangeController {
 | 
				
			||||||
		exchangeService.removeAccount(exchangeId, accountId, user);
 | 
							exchangeService.removeAccount(exchangeId, accountId, user);
 | 
				
			||||||
		return "redirect:/exchanges/" + exchangeId + "/accounts";
 | 
							return "redirect:/exchanges/" + exchangeId + "/accounts";
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@GetMapping(path = "/{exchangeId}/edit")
 | 
				
			||||||
 | 
						public String getEditPage(Model model, @PathVariable long exchangeId, @AuthenticationPrincipal User user) {
 | 
				
			||||||
 | 
							model.addAttribute("exchange", exchangeService.getData(exchangeId, user));
 | 
				
			||||||
 | 
							return "exchange/edit";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@PostMapping(path = "/{exchangeId}/edit")
 | 
				
			||||||
 | 
						public String postEdit(@PathVariable long exchangeId, @AuthenticationPrincipal User user, @Valid @ModelAttribute EditExchangePayload payload) {
 | 
				
			||||||
 | 
							exchangeService.edit(exchangeId, payload, user);
 | 
				
			||||||
 | 
							return "redirect:/exchanges/" + exchangeId;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,8 +2,11 @@ package nl.andrewl.coyotecredit.ctl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import lombok.RequiredArgsConstructor;
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
import nl.andrewl.coyotecredit.ctl.dto.RegisterPayload;
 | 
					import nl.andrewl.coyotecredit.ctl.dto.RegisterPayload;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.dao.ExchangeInvitationRepository;
 | 
				
			||||||
import nl.andrewl.coyotecredit.service.UserService;
 | 
					import nl.andrewl.coyotecredit.service.UserService;
 | 
				
			||||||
import org.springframework.stereotype.Controller;
 | 
					import org.springframework.stereotype.Controller;
 | 
				
			||||||
 | 
					import org.springframework.transaction.annotation.Transactional;
 | 
				
			||||||
 | 
					import org.springframework.ui.Model;
 | 
				
			||||||
import org.springframework.web.bind.annotation.GetMapping;
 | 
					import org.springframework.web.bind.annotation.GetMapping;
 | 
				
			||||||
import org.springframework.web.bind.annotation.ModelAttribute;
 | 
					import org.springframework.web.bind.annotation.ModelAttribute;
 | 
				
			||||||
import org.springframework.web.bind.annotation.PostMapping;
 | 
					import org.springframework.web.bind.annotation.PostMapping;
 | 
				
			||||||
| 
						 | 
					@ -13,14 +16,26 @@ import org.springframework.web.bind.annotation.RequestParam;
 | 
				
			||||||
@RequiredArgsConstructor
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
public class PublicPageController {
 | 
					public class PublicPageController {
 | 
				
			||||||
	private final UserService userService;
 | 
						private final UserService userService;
 | 
				
			||||||
 | 
						private final ExchangeInvitationRepository invitationRepository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@GetMapping(path = "/login")
 | 
						@GetMapping(path = "/login")
 | 
				
			||||||
	public String getLoginPage() {
 | 
						public String getLoginPage() {
 | 
				
			||||||
		return "public/login";
 | 
							return "public/login";
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@GetMapping(path = "/register")
 | 
						@GetMapping(path = "/register") @Transactional(readOnly = true)
 | 
				
			||||||
	public String getRegisterPage() {
 | 
						public String getRegisterPage(
 | 
				
			||||||
 | 
								Model model,
 | 
				
			||||||
 | 
								@RequestParam(name = "inviteCode", required = false) String inviteCode
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
 | 
							if (inviteCode != null && !inviteCode.isBlank()) {
 | 
				
			||||||
 | 
								invitationRepository.findByCode(inviteCode).ifPresent(invite -> {
 | 
				
			||||||
 | 
									model.addAttribute("inviteCode", inviteCode);
 | 
				
			||||||
 | 
									model.addAttribute("inviteUserEmail", invite.getUserEmail());
 | 
				
			||||||
 | 
									model.addAttribute("inviteSenderName", invite.getSender().getName());
 | 
				
			||||||
 | 
									model.addAttribute("inviteExchangeName", invite.getExchange().getName());
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return "public/register";
 | 
							return "public/register";
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +0,0 @@
 | 
				
			||||||
package nl.andrewl.coyotecredit.ctl.dto;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public record AddAccountPayload(
 | 
					 | 
				
			||||||
		String name,
 | 
					 | 
				
			||||||
		String email,
 | 
					 | 
				
			||||||
		String username,
 | 
					 | 
				
			||||||
		String password
 | 
					 | 
				
			||||||
) {}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javax.validation.constraints.NotBlank;
 | 
				
			||||||
 | 
					import javax.validation.constraints.NotNull;
 | 
				
			||||||
 | 
					import javax.validation.constraints.PositiveOrZero;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record EditExchangePayload(
 | 
				
			||||||
 | 
							@NotNull @NotBlank
 | 
				
			||||||
 | 
							String name,
 | 
				
			||||||
 | 
							String description,
 | 
				
			||||||
 | 
							boolean publiclyAccessible,
 | 
				
			||||||
 | 
							@PositiveOrZero
 | 
				
			||||||
 | 
							long primaryTradeableId,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Colors
 | 
				
			||||||
 | 
							String primaryBackgroundColor,
 | 
				
			||||||
 | 
							String secondaryBackgroundColor,
 | 
				
			||||||
 | 
							String primaryForegroundColor,
 | 
				
			||||||
 | 
							String secondaryForegroundColor
 | 
				
			||||||
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,13 @@ import java.util.List;
 | 
				
			||||||
public record FullExchangeData (
 | 
					public record FullExchangeData (
 | 
				
			||||||
		long id,
 | 
							long id,
 | 
				
			||||||
		String name,
 | 
							String name,
 | 
				
			||||||
 | 
							String description,
 | 
				
			||||||
 | 
							boolean publiclyAccessible,
 | 
				
			||||||
		TradeableData primaryTradeable,
 | 
							TradeableData primaryTradeable,
 | 
				
			||||||
 | 
							String primaryBackgroundColor,
 | 
				
			||||||
 | 
							String secondaryBackgroundColor,
 | 
				
			||||||
 | 
							String primaryForegroundColor,
 | 
				
			||||||
 | 
							String secondaryForegroundColor,
 | 
				
			||||||
		List<TradeableData> supportedTradeables,
 | 
							List<TradeableData> supportedTradeables,
 | 
				
			||||||
		String totalMarketValue,
 | 
							String totalMarketValue,
 | 
				
			||||||
		int accountCount,
 | 
							int accountCount,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record InvitationData(
 | 
				
			||||||
 | 
							long id,
 | 
				
			||||||
 | 
							long exchangeId,
 | 
				
			||||||
 | 
							String exchangeName
 | 
				
			||||||
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javax.validation.constraints.NotBlank;
 | 
				
			||||||
 | 
					import javax.validation.constraints.NotNull;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record InviteUserPayload(
 | 
				
			||||||
 | 
							@NotBlank @NotNull
 | 
				
			||||||
 | 
							String email
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -3,5 +3,8 @@ package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
public record RegisterPayload (
 | 
					public record RegisterPayload (
 | 
				
			||||||
		String username,
 | 
							String username,
 | 
				
			||||||
		String email,
 | 
							String email,
 | 
				
			||||||
		String password
 | 
							String password,
 | 
				
			||||||
 | 
							// Invite data, this may be null.
 | 
				
			||||||
 | 
							String inviteCode,
 | 
				
			||||||
 | 
							String accountName
 | 
				
			||||||
) {}
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,7 @@ package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public record SimpleAccountData (
 | 
					public record SimpleAccountData (
 | 
				
			||||||
		long id,
 | 
							long id,
 | 
				
			||||||
 | 
							long userId,
 | 
				
			||||||
		String number,
 | 
							String number,
 | 
				
			||||||
		String name,
 | 
							String name,
 | 
				
			||||||
		boolean admin,
 | 
							boolean admin,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,10 @@
 | 
				
			||||||
package nl.andrewl.coyotecredit.ctl.dto;
 | 
					package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public record UserData (
 | 
					public record UserData (
 | 
				
			||||||
		long id,
 | 
							long id,
 | 
				
			||||||
		String username,
 | 
							String username,
 | 
				
			||||||
		String email
 | 
							String email,
 | 
				
			||||||
 | 
							List<InvitationData> exchangeInvitations
 | 
				
			||||||
) {}
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.dao;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.model.ExchangeInvitation;
 | 
				
			||||||
 | 
					import org.springframework.data.jpa.repository.JpaRepository;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Repository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Optional;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Repository
 | 
				
			||||||
 | 
					public interface ExchangeInvitationRepository extends JpaRepository<ExchangeInvitation, Long> {
 | 
				
			||||||
 | 
						Optional<ExchangeInvitation> findByCode(String code);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						List<ExchangeInvitation> findAllByUserEmail(String email);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -10,4 +10,6 @@ import java.util.Optional;
 | 
				
			||||||
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);
 | 
						boolean existsByUsername(String username);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Optional<User> findByEmail(String email);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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.util.HashSet;
 | 
					import java.util.HashSet;
 | 
				
			||||||
| 
						 | 
					@ -23,15 +24,34 @@ public class Exchange {
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * The name for this exchange.
 | 
						 * The name for this exchange.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	@Column(nullable = false)
 | 
						@Column(nullable = false) @Setter
 | 
				
			||||||
	private String name;
 | 
						private String name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * The primary tradeable that's used by this exchange.
 | 
						 * The primary tradeable that's used by this exchange.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	@ManyToOne(optional = false, fetch = FetchType.LAZY)
 | 
						@ManyToOne(optional = false, fetch = FetchType.LAZY) @Setter
 | 
				
			||||||
	private Tradeable primaryTradeable;
 | 
						private Tradeable primaryTradeable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * A user-provided description for the exchange.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						@Column(length = 1024) @Setter
 | 
				
			||||||
 | 
						private String description;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column(nullable = false) @Setter
 | 
				
			||||||
 | 
						private boolean publiclyAccessible;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Colors:
 | 
				
			||||||
 | 
						@Column @Setter
 | 
				
			||||||
 | 
						private String primaryBackgroundColor;
 | 
				
			||||||
 | 
						@Column @Setter
 | 
				
			||||||
 | 
						private String secondaryBackgroundColor;
 | 
				
			||||||
 | 
						@Column @Setter
 | 
				
			||||||
 | 
						private String primaryForegroundColor;
 | 
				
			||||||
 | 
						@Column @Setter
 | 
				
			||||||
 | 
						private String secondaryForegroundColor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * The set of tradeables that this exchange allows users to interact with.
 | 
						 * The set of tradeables that this exchange allows users to interact with.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,52 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.AccessLevel;
 | 
				
			||||||
 | 
					import lombok.Getter;
 | 
				
			||||||
 | 
					import lombok.NoArgsConstructor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javax.persistence.*;
 | 
				
			||||||
 | 
					import java.time.LocalDateTime;
 | 
				
			||||||
 | 
					import java.time.ZoneOffset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Represents an invitation which an admin can send to users (or any email
 | 
				
			||||||
 | 
					 * address) to invite them to participate in an exchange. If no user with the
 | 
				
			||||||
 | 
					 * given email address exists, then a registration code will be sent to the
 | 
				
			||||||
 | 
					 * email address, which when present at the /register page, will give context
 | 
				
			||||||
 | 
					 * to allow the user to be automatically added to the exchange.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@Entity
 | 
				
			||||||
 | 
					@NoArgsConstructor(access = AccessLevel.PROTECTED)
 | 
				
			||||||
 | 
					@Getter
 | 
				
			||||||
 | 
					public class ExchangeInvitation {
 | 
				
			||||||
 | 
						@Id
 | 
				
			||||||
 | 
						@GeneratedValue(strategy = GenerationType.IDENTITY)
 | 
				
			||||||
 | 
						private Long id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@ManyToOne(optional = false, fetch = FetchType.LAZY)
 | 
				
			||||||
 | 
						private Exchange exchange;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@ManyToOne(optional = false, fetch = FetchType.LAZY)
 | 
				
			||||||
 | 
						private Account sender;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column(nullable = false)
 | 
				
			||||||
 | 
						private String userEmail;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column(nullable = false, unique = true)
 | 
				
			||||||
 | 
						private String code;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column(nullable = false)
 | 
				
			||||||
 | 
						private LocalDateTime expiresAt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ExchangeInvitation(Exchange exchange, Account sender, String userEmail, String code, LocalDateTime expiresAt) {
 | 
				
			||||||
 | 
							this.exchange = exchange;
 | 
				
			||||||
 | 
							this.sender = sender;
 | 
				
			||||||
 | 
							this.userEmail = userEmail;
 | 
				
			||||||
 | 
							this.code = code;
 | 
				
			||||||
 | 
							this.expiresAt = expiresAt;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public boolean isExpired() {
 | 
				
			||||||
 | 
							return getExpiresAt().isBefore(LocalDateTime.now(ZoneOffset.UTC));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -5,21 +5,24 @@ import nl.andrewl.coyotecredit.ctl.dto.*;
 | 
				
			||||||
import nl.andrewl.coyotecredit.dao.*;
 | 
					import nl.andrewl.coyotecredit.dao.*;
 | 
				
			||||||
import nl.andrewl.coyotecredit.model.*;
 | 
					import nl.andrewl.coyotecredit.model.*;
 | 
				
			||||||
import nl.andrewl.coyotecredit.util.AccountNumberUtils;
 | 
					import nl.andrewl.coyotecredit.util.AccountNumberUtils;
 | 
				
			||||||
 | 
					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.security.crypto.password.PasswordEncoder;
 | 
					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.math.BigDecimal;
 | 
					import java.math.BigDecimal;
 | 
				
			||||||
import java.math.RoundingMode;
 | 
					import java.math.RoundingMode;
 | 
				
			||||||
import java.text.DecimalFormat;
 | 
					import java.text.DecimalFormat;
 | 
				
			||||||
import java.time.LocalDateTime;
 | 
					import java.time.LocalDateTime;
 | 
				
			||||||
import java.time.ZoneOffset;
 | 
					import java.time.ZoneOffset;
 | 
				
			||||||
import java.util.Comparator;
 | 
					import java.util.*;
 | 
				
			||||||
import java.util.HashMap;
 | 
					 | 
				
			||||||
import java.util.List;
 | 
					 | 
				
			||||||
import java.util.Map;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Service
 | 
					@Service
 | 
				
			||||||
@RequiredArgsConstructor
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
| 
						 | 
					@ -29,8 +32,13 @@ public class ExchangeService {
 | 
				
			||||||
	private final TransactionRepository transactionRepository;
 | 
						private final TransactionRepository transactionRepository;
 | 
				
			||||||
	private final TradeableRepository tradeableRepository;
 | 
						private final TradeableRepository tradeableRepository;
 | 
				
			||||||
	private final UserRepository userRepository;
 | 
						private final UserRepository userRepository;
 | 
				
			||||||
 | 
						private final ExchangeInvitationRepository invitationRepository;
 | 
				
			||||||
 | 
						private final JavaMailSender mailSender;
 | 
				
			||||||
	private final PasswordEncoder passwordEncoder;
 | 
						private final PasswordEncoder passwordEncoder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Value("${coyote-credit.base-url}")
 | 
				
			||||||
 | 
						private String baseUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Transactional(readOnly = true)
 | 
						@Transactional(readOnly = true)
 | 
				
			||||||
	public FullExchangeData getData(long exchangeId, User user) {
 | 
						public FullExchangeData getData(long exchangeId, User user) {
 | 
				
			||||||
		Exchange e = exchangeRepository.findById(exchangeId)
 | 
							Exchange e = exchangeRepository.findById(exchangeId)
 | 
				
			||||||
| 
						 | 
					@ -46,7 +54,13 @@ public class ExchangeService {
 | 
				
			||||||
		return new FullExchangeData(
 | 
							return new FullExchangeData(
 | 
				
			||||||
				e.getId(),
 | 
									e.getId(),
 | 
				
			||||||
				e.getName(),
 | 
									e.getName(),
 | 
				
			||||||
 | 
									e.getDescription(),
 | 
				
			||||||
 | 
									e.isPubliclyAccessible(),
 | 
				
			||||||
				new TradeableData(e.getPrimaryTradeable()),
 | 
									new TradeableData(e.getPrimaryTradeable()),
 | 
				
			||||||
 | 
									e.getPrimaryBackgroundColor(),
 | 
				
			||||||
 | 
									e.getSecondaryBackgroundColor(),
 | 
				
			||||||
 | 
									e.getPrimaryForegroundColor(),
 | 
				
			||||||
 | 
									e.getSecondaryForegroundColor(),
 | 
				
			||||||
				e.getAllTradeables().stream()
 | 
									e.getAllTradeables().stream()
 | 
				
			||||||
						.map(TradeableData::new)
 | 
											.map(TradeableData::new)
 | 
				
			||||||
						.sorted(Comparator.comparing(TradeableData::symbol))
 | 
											.sorted(Comparator.comparing(TradeableData::symbol))
 | 
				
			||||||
| 
						 | 
					@ -72,6 +86,7 @@ public class ExchangeService {
 | 
				
			||||||
				.sorted(Comparator.comparing(Account::getName))
 | 
									.sorted(Comparator.comparing(Account::getName))
 | 
				
			||||||
				.map(a -> new SimpleAccountData(
 | 
									.map(a -> new SimpleAccountData(
 | 
				
			||||||
						a.getId(),
 | 
											a.getId(),
 | 
				
			||||||
 | 
											a.getUser().getId(),
 | 
				
			||||||
						a.getNumber(),
 | 
											a.getNumber(),
 | 
				
			||||||
						a.getName(),
 | 
											a.getName(),
 | 
				
			||||||
						a.isAdmin(),
 | 
											a.isAdmin(),
 | 
				
			||||||
| 
						 | 
					@ -203,9 +218,125 @@ public class ExchangeService {
 | 
				
			||||||
		return accountRepository.findAllByUser(user).stream()
 | 
							return accountRepository.findAllByUser(user).stream()
 | 
				
			||||||
				.map(a -> new ExchangeAccountData(
 | 
									.map(a -> new ExchangeAccountData(
 | 
				
			||||||
						new ExchangeData(a.getExchange().getId(), a.getExchange().getName(), a.getExchange().getPrimaryTradeable().getSymbol()),
 | 
											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()))
 | 
											new SimpleAccountData(
 | 
				
			||||||
 | 
													a.getId(),
 | 
				
			||||||
 | 
													user.getId(),
 | 
				
			||||||
 | 
													a.getNumber(),
 | 
				
			||||||
 | 
													a.getName(),
 | 
				
			||||||
 | 
													a.isAdmin(),
 | 
				
			||||||
 | 
													TradeableData.DECIMAL_FORMAT.format(a.getTotalBalance())
 | 
				
			||||||
 | 
											)
 | 
				
			||||||
				))
 | 
									))
 | 
				
			||||||
				.sorted(Comparator.comparing(d -> d.exchange().name()))
 | 
									.sorted(Comparator.comparing(d -> d.exchange().name()))
 | 
				
			||||||
				.toList();
 | 
									.toList();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Transactional
 | 
				
			||||||
 | 
						public void edit(long exchangeId, EditExchangePayload payload, User user) {
 | 
				
			||||||
 | 
							Exchange e = exchangeRepository.findById(exchangeId)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							Account account = accountRepository.findByUserAndExchange(user, e)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							if (!account.isAdmin()) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
 | 
				
			||||||
 | 
							e.setName(payload.name());
 | 
				
			||||||
 | 
							e.setDescription(payload.description());
 | 
				
			||||||
 | 
							Tradeable primaryTradeable = tradeableRepository.findById(payload.primaryTradeableId())
 | 
				
			||||||
 | 
											.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown primary tradeable currency."));
 | 
				
			||||||
 | 
							if (!e.getAllTradeables().contains(primaryTradeable)) {
 | 
				
			||||||
 | 
								throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "This exchange doesn't support " + primaryTradeable.getSymbol() + ".");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (primaryTradeable.getType().equals(TradeableType.STOCK)) {
 | 
				
			||||||
 | 
								throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid primary tradeable currency. Stocks are not permitted.");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							e.setPrimaryTradeable(primaryTradeable);
 | 
				
			||||||
 | 
							e.setPubliclyAccessible(payload.publiclyAccessible());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							e.setPrimaryBackgroundColor(payload.primaryBackgroundColor());
 | 
				
			||||||
 | 
							e.setSecondaryBackgroundColor(payload.secondaryBackgroundColor());
 | 
				
			||||||
 | 
							e.setPrimaryForegroundColor(payload.primaryForegroundColor());
 | 
				
			||||||
 | 
							e.setSecondaryForegroundColor(payload.secondaryForegroundColor());
 | 
				
			||||||
 | 
							exchangeRepository.save(e);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Transactional
 | 
				
			||||||
 | 
						public void inviteUser(long exchangeId, User user, InviteUserPayload payload) {
 | 
				
			||||||
 | 
							Exchange exchange = exchangeRepository.findById(exchangeId)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							Account account = accountRepository.findByUserAndExchange(user, exchange)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							if (!account.isAdmin()) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
 | 
				
			||||||
 | 
							LocalDateTime expiresAt = LocalDateTime.now(ZoneOffset.UTC).plusDays(7);
 | 
				
			||||||
 | 
							ExchangeInvitation invitation = invitationRepository.save(
 | 
				
			||||||
 | 
									new ExchangeInvitation(exchange, account, payload.email(), StringUtils.random(64), expiresAt));
 | 
				
			||||||
 | 
							Optional<User> invitedUser = userRepository.findByEmail(payload.email());
 | 
				
			||||||
 | 
							if (invitedUser.isEmpty()) {
 | 
				
			||||||
 | 
								try {
 | 
				
			||||||
 | 
									sendInvitationEmail(invitation);
 | 
				
			||||||
 | 
								} catch (MessagingException e) {
 | 
				
			||||||
 | 
									e.printStackTrace();
 | 
				
			||||||
 | 
									throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Could not send invitation email.");
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Transactional
 | 
				
			||||||
 | 
						public void acceptInvite(long exchangeId, long inviteId, User user) {
 | 
				
			||||||
 | 
							Exchange exchange = exchangeRepository.findById(exchangeId)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							ExchangeInvitation invite = invitationRepository.findById(inviteId)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							if (!invite.getUserEmail().equalsIgnoreCase(user.getEmail())) {
 | 
				
			||||||
 | 
								throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "This invite is for someone else.");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (!invite.getExchange().getId().equals(exchangeId)) {
 | 
				
			||||||
 | 
								throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "This invite is for a different exchange.");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// If the user already has an account, silently delete the invite.
 | 
				
			||||||
 | 
							if (accountRepository.existsByUserAndExchange(user, exchange)) {
 | 
				
			||||||
 | 
								invitationRepository.delete(invite);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (invite.isExpired()) {
 | 
				
			||||||
 | 
								throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "This invite is expired.");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// Create the account.
 | 
				
			||||||
 | 
							Account account = accountRepository.save(new Account(AccountNumberUtils.generate(), user, user.getUsername(), exchange));
 | 
				
			||||||
 | 
							invitationRepository.delete(invite);
 | 
				
			||||||
 | 
							for (var t : exchange.getAllTradeables()) {
 | 
				
			||||||
 | 
								account.getBalances().add(new Balance(account, t, BigDecimal.ZERO));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							accountRepository.save(account);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Transactional
 | 
				
			||||||
 | 
						public void rejectInvite(long exchangeId, long inviteId, User user) {
 | 
				
			||||||
 | 
							ExchangeInvitation invite = invitationRepository.findById(inviteId)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							if (!invite.getUserEmail().equalsIgnoreCase(user.getEmail())) {
 | 
				
			||||||
 | 
								throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "This invite is for someone else.");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (!invite.getExchange().getId().equals(exchangeId)) {
 | 
				
			||||||
 | 
								throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "This invite is for a different exchange.");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							invitationRepository.delete(invite);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private void sendInvitationEmail(ExchangeInvitation invitation) throws MessagingException {
 | 
				
			||||||
 | 
							MimeMessage msg = mailSender.createMimeMessage();
 | 
				
			||||||
 | 
							MimeMessageHelper helper = new MimeMessageHelper(msg);
 | 
				
			||||||
 | 
							helper.setFrom("Coyote Credit <noreply@coyote-credit.com>");
 | 
				
			||||||
 | 
							helper.setTo(invitation.getUserEmail());
 | 
				
			||||||
 | 
							helper.setSubject("Exchange Invitation");
 | 
				
			||||||
 | 
							String url = baseUrl + "/register?inviteCode=" + invitation.getCode();
 | 
				
			||||||
 | 
							helper.setText(String.format(
 | 
				
			||||||
 | 
									"""
 | 
				
			||||||
 | 
									<p>You have been invited by %s to join %s on Coyote Credit.
 | 
				
			||||||
 | 
									Click the link below to register an account.</p>
 | 
				
			||||||
 | 
									<a href="%s">%s</a>
 | 
				
			||||||
 | 
									""",
 | 
				
			||||||
 | 
									invitation.getSender().getName(),
 | 
				
			||||||
 | 
									invitation.getExchange().getName(),
 | 
				
			||||||
 | 
									url, url
 | 
				
			||||||
 | 
							), true);
 | 
				
			||||||
 | 
							mailSender.send(msg);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,12 +1,18 @@
 | 
				
			||||||
package nl.andrewl.coyotecredit.service;
 | 
					package nl.andrewl.coyotecredit.service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import lombok.RequiredArgsConstructor;
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.ctl.dto.InvitationData;
 | 
				
			||||||
import nl.andrewl.coyotecredit.ctl.dto.RegisterPayload;
 | 
					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.AccountRepository;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.dao.ExchangeInvitationRepository;
 | 
				
			||||||
import nl.andrewl.coyotecredit.dao.UserActivationTokenRepository;
 | 
					import nl.andrewl.coyotecredit.dao.UserActivationTokenRepository;
 | 
				
			||||||
import nl.andrewl.coyotecredit.dao.UserRepository;
 | 
					import nl.andrewl.coyotecredit.dao.UserRepository;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.model.Account;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.model.Balance;
 | 
				
			||||||
import nl.andrewl.coyotecredit.model.User;
 | 
					import nl.andrewl.coyotecredit.model.User;
 | 
				
			||||||
import nl.andrewl.coyotecredit.model.UserActivationToken;
 | 
					import nl.andrewl.coyotecredit.model.UserActivationToken;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.util.AccountNumberUtils;
 | 
				
			||||||
import nl.andrewl.coyotecredit.util.StringUtils;
 | 
					import nl.andrewl.coyotecredit.util.StringUtils;
 | 
				
			||||||
import org.springframework.beans.factory.annotation.Value;
 | 
					import org.springframework.beans.factory.annotation.Value;
 | 
				
			||||||
import org.springframework.http.HttpStatus;
 | 
					import org.springframework.http.HttpStatus;
 | 
				
			||||||
| 
						 | 
					@ -20,14 +26,20 @@ import org.springframework.web.server.ResponseStatusException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import javax.mail.MessagingException;
 | 
					import javax.mail.MessagingException;
 | 
				
			||||||
import javax.mail.internet.MimeMessage;
 | 
					import javax.mail.internet.MimeMessage;
 | 
				
			||||||
 | 
					import java.math.BigDecimal;
 | 
				
			||||||
import java.time.LocalDateTime;
 | 
					import java.time.LocalDateTime;
 | 
				
			||||||
import java.time.ZoneOffset;
 | 
					import java.time.ZoneOffset;
 | 
				
			||||||
 | 
					import java.util.ArrayList;
 | 
				
			||||||
 | 
					import java.util.Comparator;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Service
 | 
					@Service
 | 
				
			||||||
@RequiredArgsConstructor
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
public class UserService {
 | 
					public class UserService {
 | 
				
			||||||
	private final UserRepository userRepository;
 | 
						private final UserRepository userRepository;
 | 
				
			||||||
 | 
						private final AccountRepository accountRepository;
 | 
				
			||||||
	private final UserActivationTokenRepository activationTokenRepository;
 | 
						private final UserActivationTokenRepository activationTokenRepository;
 | 
				
			||||||
 | 
						private final ExchangeInvitationRepository exchangeInvitationRepository;
 | 
				
			||||||
	private final JavaMailSender mailSender;
 | 
						private final JavaMailSender mailSender;
 | 
				
			||||||
	private final PasswordEncoder passwordEncoder;
 | 
						private final PasswordEncoder passwordEncoder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,7 +54,13 @@ 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(), user.getEmail());
 | 
							List<InvitationData> exchangeInvitations = new ArrayList<>();
 | 
				
			||||||
 | 
							for (var invitation : exchangeInvitationRepository.findAllByUserEmail(user.getEmail())) {
 | 
				
			||||||
 | 
								if (invitation.isExpired()) continue;
 | 
				
			||||||
 | 
								exchangeInvitations.add(new InvitationData(invitation.getId(), invitation.getExchange().getId(), invitation.getExchange().getName()));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							exchangeInvitations.sort(Comparator.comparing(InvitationData::id));
 | 
				
			||||||
 | 
							return new UserData(user.getId(), user.getUsername(), user.getEmail(), exchangeInvitations);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Transactional
 | 
						@Transactional
 | 
				
			||||||
| 
						 | 
					@ -52,6 +70,26 @@ public class UserService {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		User user = new User(payload.username(), passwordEncoder.encode(payload.password()), payload.email());
 | 
							User user = new User(payload.username(), passwordEncoder.encode(payload.password()), payload.email());
 | 
				
			||||||
		user = userRepository.save(user);
 | 
							user = userRepository.save(user);
 | 
				
			||||||
 | 
							if (payload.inviteCode() != null) {
 | 
				
			||||||
 | 
								var invite = exchangeInvitationRepository.findByCode(payload.inviteCode())
 | 
				
			||||||
 | 
										.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid invitation code."));
 | 
				
			||||||
 | 
								if (!invite.getUserEmail().equalsIgnoreCase(user.getEmail())) {
 | 
				
			||||||
 | 
									throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "This invitation code is for somebody else.");
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (invite.isExpired()) {
 | 
				
			||||||
 | 
									throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "This invitation code is expired.");
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								exchangeInvitationRepository.delete(invite);
 | 
				
			||||||
 | 
								Account account = new Account(AccountNumberUtils.generate(), user, payload.accountName(), invite.getExchange());
 | 
				
			||||||
 | 
								account = accountRepository.save(account);
 | 
				
			||||||
 | 
								for (var tradeable : invite.getExchange().getAllTradeables()) {
 | 
				
			||||||
 | 
									account.getBalances().add(new Balance(account, tradeable, BigDecimal.ZERO));
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								account = accountRepository.save(account);
 | 
				
			||||||
 | 
								user.getAccounts().add(account);
 | 
				
			||||||
 | 
								user = userRepository.save(user);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		String token = StringUtils.random(64);
 | 
							String token = StringUtils.random(64);
 | 
				
			||||||
		LocalDateTime expiresAt = LocalDateTime.now(ZoneOffset.UTC).plusHours(24);
 | 
							LocalDateTime expiresAt = LocalDateTime.now(ZoneOffset.UTC).plusHours(24);
 | 
				
			||||||
		UserActivationToken activationToken = new UserActivationToken(token, user, expiresAt);
 | 
							UserActivationToken activationToken = new UserActivationToken(token, user, expiresAt);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
<html
 | 
					<html
 | 
				
			||||||
        lang="en"
 | 
					        lang="en"
 | 
				
			||||||
        xmlns:th="http://www.thymeleaf.org"
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
        th:replace="~{layout/basic_page :: layout (title='Add Account', content=~{::#content})}"
 | 
					        th:replace="~{layout/basic_page :: layout (title='Edit Balances', content=~{::#content})}"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
<div id="content" class="container">
 | 
					<div id="content" class="container">
 | 
				
			||||||
    <h1>Edit Balances</h1>
 | 
					    <h1>Edit Balances</h1>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
<html
 | 
					<html
 | 
				
			||||||
        lang="en"
 | 
					        lang="en"
 | 
				
			||||||
        xmlns:th="http://www.thymeleaf.org"
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
        th:replace="~{layout/basic_page :: layout (title='Add Account', content=~{::#content})}"
 | 
					        th:replace="~{layout/basic_page :: layout (title='Transfer', content=~{::#content})}"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
<div id="content" class="container">
 | 
					<div id="content" class="container">
 | 
				
			||||||
    <h1 class="display-4">Transfer</h1>
 | 
					    <h1 class="display-4">Transfer</h1>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
<html
 | 
					<html
 | 
				
			||||||
        lang="en"
 | 
					        lang="en"
 | 
				
			||||||
        xmlns:th="http://www.thymeleaf.org"
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
        th:replace="~{layout/basic_page :: layout (title='Home', content=~{::#content})}"
 | 
					        th:replace="~{layout/basic_page :: layout (title='Error', content=~{::#content})}"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
<div id="content" class="container">
 | 
					<div id="content" class="container">
 | 
				
			||||||
    <div class="row justify-content-center">
 | 
					    <div class="row justify-content-center">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,14 +19,18 @@
 | 
				
			||||||
        </thead>
 | 
					        </thead>
 | 
				
			||||||
        <tbody>
 | 
					        <tbody>
 | 
				
			||||||
            <tr th:each="account : ${accounts}">
 | 
					            <tr th:each="account : ${accounts}">
 | 
				
			||||||
                <td><a th:href="@{/accounts/{id}(id=${account.id()})}" th:text="${account.number()}"></a></td>
 | 
					                <td><a class="colored-link" 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 class="monospace" th:text="${account.totalBalance()}"></td>
 | 
					                <td class="monospace" th:text="${account.totalBalance()}"></td>
 | 
				
			||||||
                <td><a th:href="@{/exchanges/{eId}/removeAccount/{aId}(eId=${exchangeId}, aId=${account.id()})}">Remove</a></td>
 | 
					                <td><a
 | 
				
			||||||
 | 
					                        class="colored-link"
 | 
				
			||||||
 | 
					                        th:href="@{/exchanges/{eId}/removeAccount/{aId}(eId=${exchangeId}, aId=${account.id()})}"
 | 
				
			||||||
 | 
					                        th:if="${account.userId() != #authentication.getPrincipal().getId()}"
 | 
				
			||||||
 | 
					                >Remove</a></td>
 | 
				
			||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
        </tbody>
 | 
					        </tbody>
 | 
				
			||||||
    </table>
 | 
					    </table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <a class="btn btn-primary" th:href="@{/exchanges/{eId}/addAccount(eId=${exchangeId})}">Add Account</a>
 | 
					    <a class="btn btn-primary" th:href="@{/exchanges/{eId}/inviteUser(eId=${exchangeId})}">Invite User</a>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,33 +0,0 @@
 | 
				
			||||||
<!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">
 | 
					 | 
				
			||||||
    <h1>Add Account</h1>
 | 
					 | 
				
			||||||
    <p>
 | 
					 | 
				
			||||||
        Use this page to add an account to the exchange.
 | 
					 | 
				
			||||||
    </p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <form th:href="@{/exchanges/{eId}/addAccount(eId=${exchangeId})}" th:method="post">
 | 
					 | 
				
			||||||
        <div class="mb-3">
 | 
					 | 
				
			||||||
            <label for="nameInput" class="form-label">Name</label>
 | 
					 | 
				
			||||||
            <input id="nameInput" type="text" class="form-control" name="name" required/>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <div class="mb-3">
 | 
					 | 
				
			||||||
            <label for="usernameInput" class="form-label">Username</label>
 | 
					 | 
				
			||||||
            <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 class="mb-3">
 | 
					 | 
				
			||||||
            <label for="passwordInput" class="form-label">Password</label>
 | 
					 | 
				
			||||||
            <input id="passwordInput" type="password" class="form-control" name="password" required/>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <input type="hidden" name="_csrf" th:value="${_csrf.getToken()}"/>
 | 
					 | 
				
			||||||
        <button type="submit" class="btn btn-primary">Submit</button>
 | 
					 | 
				
			||||||
    </form>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,85 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html
 | 
				
			||||||
 | 
					        lang="en"
 | 
				
			||||||
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
 | 
					        th:replace="~{layout/basic_page :: layout (title='Edit Exchange', content=~{::#content})}"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					<div id="content" class="container">
 | 
				
			||||||
 | 
					    <h1 class="display-4">Edit Exchange Settings</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <form th:action="@{/exchanges/{eId}/edit(eId=${exchange.id()})}" th:method="post">
 | 
				
			||||||
 | 
					        <div class="row">
 | 
				
			||||||
 | 
					            <div class="col-md-4">
 | 
				
			||||||
 | 
					                <div class="mb-3">
 | 
				
			||||||
 | 
					                    <label for="nameInput" class="form-label">Name</label>
 | 
				
			||||||
 | 
					                    <input type="text" name="name" id="nameInput" class="form-control" th:value="${exchange.name()}"/>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="mb-3">
 | 
				
			||||||
 | 
					                    <label for="descriptionInput" class="form-label">Description</label>
 | 
				
			||||||
 | 
					                    <textarea id="descriptionInput" name="description" class="form-control" rows="3" maxlength="1024" th:text="${exchange.description()}"></textarea>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="form-check form-switch">
 | 
				
			||||||
 | 
					                    <input class="form-check-input" type="checkbox" name="publiclyAccessible" id="publiclyAccessibleCheck" th:checked="${exchange.publiclyAccessible()}"/>
 | 
				
			||||||
 | 
					                    <input type="hidden" value="on" name="_publiclyAccessible"/>
 | 
				
			||||||
 | 
					                    <label class="form-check-label" for="publiclyAccessibleCheck">Publicly Accessible</label>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="col-md-4">
 | 
				
			||||||
 | 
					                <label for="primaryTradeableIdSelect" class="form-label">Primary Currency</label>
 | 
				
			||||||
 | 
					                <select name="primaryTradeableId" id="primaryTradeableIdSelect" class="form-select">
 | 
				
			||||||
 | 
					                    <option
 | 
				
			||||||
 | 
					                        th:each="tb : ${exchange.supportedTradeables()}"
 | 
				
			||||||
 | 
					                        th:if="${tb.type().equals('FIAT') || tb.type().equals('CRYPTO')}"
 | 
				
			||||||
 | 
					                        th:text="${tb.symbol() + ' ' + tb.name()}"
 | 
				
			||||||
 | 
					                        th:value="${tb.id()}"
 | 
				
			||||||
 | 
					                        th:selected="${tb.id() == exchange.primaryTradeable().id()}"
 | 
				
			||||||
 | 
					                    ></option>
 | 
				
			||||||
 | 
					                </select>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="col-md-4">
 | 
				
			||||||
 | 
					                <div class="mb-3">
 | 
				
			||||||
 | 
					                    <label for="primaryBackgroundColorInput" class="form-label">Primary Background Color</label>
 | 
				
			||||||
 | 
					                    <input
 | 
				
			||||||
 | 
					                            name="primaryBackgroundColor"
 | 
				
			||||||
 | 
					                            id="primaryBackgroundColorInput"
 | 
				
			||||||
 | 
					                            type="color"
 | 
				
			||||||
 | 
					                            class="form-control form-control-color"
 | 
				
			||||||
 | 
					                            th:value="${exchange.primaryBackgroundColor()}"
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="mb-3">
 | 
				
			||||||
 | 
					                    <label for="secondaryBackgroundColorInput" class="form-label">Secondary Background Color</label>
 | 
				
			||||||
 | 
					                    <input
 | 
				
			||||||
 | 
					                            name="secondaryBackgroundColor"
 | 
				
			||||||
 | 
					                            id="secondaryBackgroundColorInput"
 | 
				
			||||||
 | 
					                            type="color"
 | 
				
			||||||
 | 
					                            class="form-control form-control-color"
 | 
				
			||||||
 | 
					                            th:value="${exchange.secondaryBackgroundColor()}"
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="mb-3">
 | 
				
			||||||
 | 
					                    <label for="primaryForegroundColorInput" class="form-label">Primary Foreground Color</label>
 | 
				
			||||||
 | 
					                    <input
 | 
				
			||||||
 | 
					                            name="primaryForegroundColor"
 | 
				
			||||||
 | 
					                            id="primaryForegroundColorInput"
 | 
				
			||||||
 | 
					                            type="color"
 | 
				
			||||||
 | 
					                            class="form-control form-control-color"
 | 
				
			||||||
 | 
					                            th:value="${exchange.primaryForegroundColor()}"
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="mb-3">
 | 
				
			||||||
 | 
					                    <label for="secondaryForegroundColorInput" class="form-label">Secondary Foreground Color</label>
 | 
				
			||||||
 | 
					                    <input
 | 
				
			||||||
 | 
					                            name="secondaryForegroundColor"
 | 
				
			||||||
 | 
					                            id="secondaryForegroundColorInput"
 | 
				
			||||||
 | 
					                            type="color"
 | 
				
			||||||
 | 
					                            class="form-control form-control-color"
 | 
				
			||||||
 | 
					                            th:value="${exchange.secondaryForegroundColor()}"
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <button type="submit" class="btn btn-primary">Save</button>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
<div id="content" class="container">
 | 
					<div id="content" class="container">
 | 
				
			||||||
    <h1 class="display-4" th:text="${exchange.name()}"></h1>
 | 
					    <h1 class="display-4" th:text="${exchange.name()}"></h1>
 | 
				
			||||||
 | 
					    <p class="lead" th:if="${exchange.description() != null && !exchange.description().isBlank()}" th:text="${exchange.description()}"></p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="card text-white bg-dark mb-3">
 | 
					    <div class="card text-white bg-dark mb-3">
 | 
				
			||||||
        <div class="card-body">
 | 
					        <div class="card-body">
 | 
				
			||||||
| 
						 | 
					@ -22,10 +23,13 @@
 | 
				
			||||||
                <dt class="col-sm-6">Number of Accounts</dt>
 | 
					                <dt class="col-sm-6">Number of Accounts</dt>
 | 
				
			||||||
                <dd class="col-sm-6" th:text="${exchange.accountCount()}"></dd>
 | 
					                <dd class="col-sm-6" th:text="${exchange.accountCount()}"></dd>
 | 
				
			||||||
            </dl>
 | 
					            </dl>
 | 
				
			||||||
 | 
					            <a class="btn btn-primary" th:href="@{/accounts/{aId}(aId=${exchange.accountId()})}">My Account</a>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <h3>Tradeable Assets</h3>
 | 
					    <div class="card text-white bg-dark mb-3">
 | 
				
			||||||
 | 
					        <div class="card-body">
 | 
				
			||||||
 | 
					            <h5 class="card-title">Tradeable Assets</h5>
 | 
				
			||||||
            <table class="table table-dark">
 | 
					            <table class="table table-dark">
 | 
				
			||||||
                <thead>
 | 
					                <thead>
 | 
				
			||||||
                <tr>
 | 
					                <tr>
 | 
				
			||||||
| 
						 | 
					@ -44,9 +48,14 @@
 | 
				
			||||||
                </tr>
 | 
					                </tr>
 | 
				
			||||||
                </tbody>
 | 
					                </tbody>
 | 
				
			||||||
            </table>
 | 
					            </table>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div>
 | 
					    <div class="card text-white bg-dark mb-3" th:if="${exchange.accountAdmin()}">
 | 
				
			||||||
        <a class="btn btn-primary" th:href="@{/accounts/{aId}(aId=${exchange.accountId()})}">My Account</a>
 | 
					        <div class="card-body">
 | 
				
			||||||
        <a class="btn btn-primary" th:if="${exchange.accountAdmin()}" th:href="@{/exchanges/{eId}/accounts(eId=${exchange.id()})}">All Accounts</a>
 | 
					            <h5 class="card-title">Administrator Tools</h5>
 | 
				
			||||||
 | 
					            <a class="btn btn-primary" th:href="@{/exchanges/{eId}/accounts(eId=${exchange.id()})}">View All Accounts</a>
 | 
				
			||||||
 | 
					            <a class="btn btn-secondary" th:href="@{/exchanges/{eId}/edit(eId=${exchange.id()})}">Edit Exchange Settings</a>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html
 | 
				
			||||||
 | 
					        lang="en"
 | 
				
			||||||
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
 | 
					        th:replace="~{layout/basic_page :: layout (title='Invite User', content=~{::#content})}"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					<div id="content" class="container">
 | 
				
			||||||
 | 
					    <h1 class="display-4">Invite User</h1>
 | 
				
			||||||
 | 
					    <p class="lead">
 | 
				
			||||||
 | 
					        Invite someone to join your exchange. If they've already got an account,
 | 
				
			||||||
 | 
					        their account will receive an invitation, or if not, they'll receive an
 | 
				
			||||||
 | 
					        email so they can create an account.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <form th:href="@{/exchanges/{eId}/inviteUser(eId=${exchangeId})}" th:method="post">
 | 
				
			||||||
 | 
					        <div class="mb-3">
 | 
				
			||||||
 | 
					            <label for="emailInput" class="form-label">Email</label>
 | 
				
			||||||
 | 
					            <input id="emailInput" name="email" type="email" class="form-control" required/>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <input type="hidden" name="_csrf" th:value="${_csrf.getToken()}"/>
 | 
				
			||||||
 | 
					        <button type="submit" class="btn btn-primary">Invite</button>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
<html
 | 
					<html
 | 
				
			||||||
        lang="en"
 | 
					        lang="en"
 | 
				
			||||||
        xmlns:th="http://www.thymeleaf.org"
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
        th:replace="~{layout/basic_page :: layout (title='Add Account', content=~{::#content})}"
 | 
					        th:replace="~{layout/basic_page :: layout (title='Remove Account', content=~{::#content})}"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
<div id="content" class="container">
 | 
					<div id="content" class="container">
 | 
				
			||||||
    <h1>Remove Account</h1>
 | 
					    <h1>Remove Account</h1>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,15 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <div class="mb-3">
 | 
					                <div class="mb-3">
 | 
				
			||||||
                    <label for="emailInput" class="form-label">Email</label>
 | 
					                    <label for="emailInput" class="form-label">Email</label>
 | 
				
			||||||
                    <input name="email" id="emailInput" class="form-control" type="email" required/>
 | 
					                    <input
 | 
				
			||||||
 | 
					                            name="email"
 | 
				
			||||||
 | 
					                            id="emailInput"
 | 
				
			||||||
 | 
					                            class="form-control"
 | 
				
			||||||
 | 
					                            type="email"
 | 
				
			||||||
 | 
					                            required
 | 
				
			||||||
 | 
					                            th:readonly="${inviteUserEmail != null}"
 | 
				
			||||||
 | 
					                            th:value="${inviteUserEmail == null ? '' : inviteUserEmail}"
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <div class="mb-3">
 | 
					                <div class="mb-3">
 | 
				
			||||||
| 
						 | 
					@ -24,7 +32,21 @@
 | 
				
			||||||
                    <input name="password" id="passwordInput" class="form-control" type="password" required/>
 | 
					                    <input name="password" id="passwordInput" class="form-control" type="password" required/>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <input type="submit" class="btn btn-primary" value="Register">
 | 
					                <div class="mb-3" th:if="${inviteCode != null}">
 | 
				
			||||||
 | 
					                    <label for="accountNameInput" class="form-label">Account Name</label>
 | 
				
			||||||
 | 
					                    <small class="text-muted">
 | 
				
			||||||
 | 
					                        You have been invited by <span th:text="${inviteSenderName}"></span> to join <span th:text="${inviteExchangeName}"></span>.
 | 
				
			||||||
 | 
					                    </small>
 | 
				
			||||||
 | 
					                    <input name="accountName" id="accountNameInput" class="form-control" type="text" required/>
 | 
				
			||||||
 | 
					                    <input name="inviteCode" type="hidden" th:value="${inviteCode}"/>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <input type="submit" class="btn btn-primary mb-3" value="Register">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <p class="alert alert-dark">
 | 
				
			||||||
 | 
					                    After registering, you will receive an email with a verification link. Please
 | 
				
			||||||
 | 
					                    click that link to verify your account before attempting to log in.
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
            </form>
 | 
					            </form>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
<html
 | 
					<html
 | 
				
			||||||
        lang="en"
 | 
					        lang="en"
 | 
				
			||||||
        xmlns:th="http://www.thymeleaf.org"
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
        th:replace="~{layout/basic_page :: layout (title='Account', content=~{::#content})}"
 | 
					        th:replace="~{layout/basic_page :: layout (title='Tradeable', content=~{::#content})}"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
<div id="content" class="container">
 | 
					<div id="content" class="container">
 | 
				
			||||||
    <h1>
 | 
					    <h1>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,4 +19,22 @@
 | 
				
			||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
        </tbody>
 | 
					        </tbody>
 | 
				
			||||||
    </table>
 | 
					    </table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="card text-white bg-dark mb-3">
 | 
				
			||||||
 | 
					        <div class="card-body">
 | 
				
			||||||
 | 
					            <h5 class="card-title">Exchange Invitations</h5>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <ul class="list-group list-group-flush">
 | 
				
			||||||
 | 
					            <li class="list-group-item list-group-item-dark" th:each="invite : ${user.exchangeInvitations()}">
 | 
				
			||||||
 | 
					                <span th:text="${invite.exchangeName()}"></span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <form th:action="@{/exchanges/{eId}/rejectInvite/{iId}(eId=${invite.exchangeId()}, iId=${invite.id()})}" method="post" class="float-end">
 | 
				
			||||||
 | 
					                    <button type="submit" class="btn btn-danger">Reject</button>
 | 
				
			||||||
 | 
					                </form>
 | 
				
			||||||
 | 
					                <form th:action="@{/exchanges/{eId}/acceptInvite/{iId}(eId=${invite.exchangeId()}, iId=${invite.id()})}" method="post" class="float-end">
 | 
				
			||||||
 | 
					                    <button type="submit" class="btn btn-success">Accept</button>
 | 
				
			||||||
 | 
					                </form>
 | 
				
			||||||
 | 
					            </li>
 | 
				
			||||||
 | 
					        </ul>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue