Expanded user-derived database models.

This commit is contained in:
Andrew Lalis 2023-02-06 10:08:35 +01:00
parent 8c2a84755d
commit 9218a38850
9 changed files with 202 additions and 4 deletions

View File

@ -0,0 +1,34 @@
package nl.andrewlalis.gymboard_api.domains.auth.controller;
import nl.andrewlalis.gymboard_api.domains.auth.dto.UserPersonalDetailsPayload;
import nl.andrewlalis.gymboard_api.domains.auth.dto.UserPersonalDetailsResponse;
import nl.andrewlalis.gymboard_api.domains.auth.dto.UserResponse;
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
import nl.andrewlalis.gymboard_api.domains.auth.service.UserService;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping(path = "/auth/me/personal-details")
public UserPersonalDetailsResponse getMyPersonalDetails(@AuthenticationPrincipal User user) {
return userService.getPersonalDetails(user.getId());
}
@PostMapping(path = "/auth/me/personal-details")
public UserResponse updateMyPersonalDetails(
@AuthenticationPrincipal User user,
@RequestBody UserPersonalDetailsPayload payload
) {
return userService.updatePersonalDetails(user.getId(), payload);
}
}

View File

@ -0,0 +1,8 @@
package nl.andrewlalis.gymboard_api.domains.auth.dao;
import nl.andrewlalis.gymboard_api.domains.auth.model.UserPersonalDetails;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserPersonalDetailsRepository extends JpaRepository<UserPersonalDetails, String> {}

View File

@ -0,0 +1,10 @@
package nl.andrewlalis.gymboard_api.domains.auth.dto;
import java.time.LocalDate;
public record UserPersonalDetailsPayload(
LocalDate birthDate,
Float currentWeight,
String currentWeightUnit,
String sex
) {}

View File

@ -0,0 +1,25 @@
package nl.andrewlalis.gymboard_api.domains.auth.dto;
import nl.andrewlalis.gymboard_api.domains.auth.model.UserPersonalDetails;
import java.time.format.DateTimeFormatter;
public record UserPersonalDetailsResponse(
String userId,
String birthDate,
float currentWeight,
String currentWeightUnit,
float currentMetricWeight,
String sex
) {
public UserPersonalDetailsResponse(UserPersonalDetails pd) {
this(
pd.getUserId(),
pd.getBirthDate().format(DateTimeFormatter.ISO_LOCAL_DATE),
pd.getCurrentWeight().floatValue(),
pd.getCurrentWeightUnit().name(),
pd.getCurrentMetricWeight().floatValue(),
pd.getSex().name()
);
}
}

View File

@ -40,6 +40,9 @@ public class User {
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, optional = false, fetch = FetchType.LAZY) @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, optional = false, fetch = FetchType.LAZY)
private UserPersonalDetails personalDetails; private UserPersonalDetails personalDetails;
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, optional = false, fetch = FetchType.LAZY)
private UserPreferences preferences;
public User() {} public User() {}
public User(String id, boolean activated, String email, String passwordHash, String name) { public User(String id, boolean activated, String email, String passwordHash, String name) {
@ -50,6 +53,7 @@ public class User {
this.name = name; this.name = name;
this.roles = new HashSet<>(); this.roles = new HashSet<>();
this.personalDetails = new UserPersonalDetails(this); this.personalDetails = new UserPersonalDetails(this);
this.preferences = new UserPreferences(this);
} }
public String getId() { public String getId() {
@ -91,4 +95,8 @@ public class User {
public UserPersonalDetails getPersonalDetails() { public UserPersonalDetails getPersonalDetails() {
return personalDetails; return personalDetails;
} }
public UserPreferences getPreferences() {
return preferences;
}
} }

View File

@ -15,7 +15,19 @@ public class UserPersonalDetails {
public enum PersonSex { public enum PersonSex {
MALE, MALE,
FEMALE, FEMALE,
UNKNOWN UNKNOWN;
public static PersonSex parse(String s) {
if (s != null && !s.isBlank()) {
s = s.strip().toUpperCase();
if (s.equals("M") || s.equals("MALE") || s.equals("MAN")) {
return MALE;
} else if (s.equals("F") || s.equals("FEMALE") || s.equals("WOMAN")) {
return FEMALE;
}
}
return UNKNOWN;
}
} }
@Id @Id
@ -50,6 +62,10 @@ public class UserPersonalDetails {
this.userId = user.getId(); this.userId = user.getId();
} }
public String getUserId() {
return userId;
}
public User getUser() { public User getUser() {
return user; return user;
} }

View File

@ -0,0 +1,60 @@
package nl.andrewlalis.gymboard_api.domains.auth.model;
import jakarta.persistence.*;
@Entity
@Table(name = "auth_user_preferences")
public class UserPreferences {
@Id
@Column(name = "user_id", length = 26)
private String userId;
@OneToOne(optional = false, fetch = FetchType.LAZY)
@PrimaryKeyJoinColumn(name = "user_id", referencedColumnName = "id")
private User user;
/**
* Flag which, if true, indicates that the user's account is private, and
* only approved users may view certain information about them.
*/
@Column(nullable = false)
private boolean accountPrivate = false;
/**
* The user's preferred locale. This should always refer to one of the
* available locales offered by the front-end app.
*/
@Column(nullable = false)
private String locale = "en-US";
public UserPreferences() {}
public UserPreferences(User user) {
this.user = user;
this.userId = user.getId();
}
public String getUserId() {
return userId;
}
public User getUser() {
return user;
}
public boolean isAccountPrivate() {
return accountPrivate;
}
public void setAccountPrivate(boolean accountPrivate) {
this.accountPrivate = accountPrivate;
}
public String getLocale() {
return locale;
}
public void setLocale(String locale) {
this.locale = locale;
}
}

View File

@ -2,13 +2,16 @@ package nl.andrewlalis.gymboard_api.domains.auth.service;
import jakarta.mail.MessagingException; import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMessage;
import nl.andrewlalis.gymboard_api.domains.api.model.WeightUnit;
import nl.andrewlalis.gymboard_api.domains.auth.dao.PasswordResetCodeRepository; import nl.andrewlalis.gymboard_api.domains.auth.dao.PasswordResetCodeRepository;
import nl.andrewlalis.gymboard_api.domains.auth.dto.*;
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserActivationCodeRepository; import nl.andrewlalis.gymboard_api.domains.auth.dao.UserActivationCodeRepository;
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserPersonalDetailsRepository;
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserRepository; import nl.andrewlalis.gymboard_api.domains.auth.dao.UserRepository;
import nl.andrewlalis.gymboard_api.domains.auth.dto.*;
import nl.andrewlalis.gymboard_api.domains.auth.model.PasswordResetCode; import nl.andrewlalis.gymboard_api.domains.auth.model.PasswordResetCode;
import nl.andrewlalis.gymboard_api.domains.auth.model.User; import nl.andrewlalis.gymboard_api.domains.auth.model.User;
import nl.andrewlalis.gymboard_api.domains.auth.model.UserActivationCode; import nl.andrewlalis.gymboard_api.domains.auth.model.UserActivationCode;
import nl.andrewlalis.gymboard_api.domains.auth.model.UserPersonalDetails;
import nl.andrewlalis.gymboard_api.util.StringGenerator; import nl.andrewlalis.gymboard_api.util.StringGenerator;
import nl.andrewlalis.gymboard_api.util.ULID; import nl.andrewlalis.gymboard_api.util.ULID;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -23,6 +26,7 @@ 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 java.math.BigDecimal;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Random; import java.util.Random;
@ -33,6 +37,7 @@ public class UserService {
private static final Logger log = LoggerFactory.getLogger(UserService.class); private static final Logger log = LoggerFactory.getLogger(UserService.class);
private final UserRepository userRepository; private final UserRepository userRepository;
private final UserPersonalDetailsRepository userPersonalDetailsRepository;
private final UserActivationCodeRepository activationCodeRepository; private final UserActivationCodeRepository activationCodeRepository;
private final PasswordResetCodeRepository passwordResetCodeRepository; private final PasswordResetCodeRepository passwordResetCodeRepository;
private final ULID ulid; private final ULID ulid;
@ -44,13 +49,14 @@ public class UserService {
public UserService( public UserService(
UserRepository userRepository, UserRepository userRepository,
UserActivationCodeRepository activationCodeRepository, UserPersonalDetailsRepository userPersonalDetailsRepository, UserActivationCodeRepository activationCodeRepository,
PasswordResetCodeRepository passwordResetCodeRepository, PasswordResetCodeRepository passwordResetCodeRepository,
ULID ulid, ULID ulid,
PasswordEncoder passwordEncoder, PasswordEncoder passwordEncoder,
JavaMailSender mailSender JavaMailSender mailSender
) { ) {
this.userRepository = userRepository; this.userRepository = userRepository;
this.userPersonalDetailsRepository = userPersonalDetailsRepository;
this.activationCodeRepository = activationCodeRepository; this.activationCodeRepository = activationCodeRepository;
this.passwordResetCodeRepository = passwordResetCodeRepository; this.passwordResetCodeRepository = passwordResetCodeRepository;
this.ulid = ulid; this.ulid = ulid;
@ -220,4 +226,36 @@ public class UserService {
LocalDateTime activationCodeCutoff = LocalDateTime.now().minus(UserActivationCode.VALID_FOR); LocalDateTime activationCodeCutoff = LocalDateTime.now().minus(UserActivationCode.VALID_FOR);
activationCodeRepository.deleteAllByCreatedAtBefore(activationCodeCutoff); activationCodeRepository.deleteAllByCreatedAtBefore(activationCodeCutoff);
} }
@Transactional
public UserResponse updatePersonalDetails(String id, UserPersonalDetailsPayload payload) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
var pd = user.getPersonalDetails();
pd.setBirthDate(payload.birthDate());
BigDecimal currentWeight = payload.currentWeight() == null ? null : BigDecimal.valueOf(payload.currentWeight());
WeightUnit currentWeightUnit = WeightUnit.parse(payload.currentWeightUnit());
BigDecimal currentMetricWeight = null;
if (currentWeight != null) {
if (currentWeightUnit == WeightUnit.POUNDS) {
currentMetricWeight = WeightUnit.toKilograms(currentWeight);
} else {
currentMetricWeight = new BigDecimal(currentWeight.toString());
}
}
pd.setCurrentWeight(currentWeight);
pd.setCurrentWeightUnit(currentWeightUnit);
pd.setCurrentMetricWeight(currentMetricWeight);
pd.setSex(UserPersonalDetails.PersonSex.parse(payload.sex()));
user = userRepository.save(user);
return new UserResponse(user);
}
@Transactional(readOnly = true)
public UserPersonalDetailsResponse getPersonalDetails(String id) {
var pd = userPersonalDetailsRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
return new UserPersonalDetailsResponse(pd);
}
} }

View File

@ -3,7 +3,6 @@ package nl.andrewlalis.gymboardsearch.config;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter; import org.springframework.web.filter.CorsFilter;