Added additional user data.
This commit is contained in:
parent
3ec052b442
commit
3a0bae209b
|
@ -0,0 +1,38 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.config;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for properly formatting error responses for exceptions that rest
|
||||||
|
* controllers in this service may throw.
|
||||||
|
*/
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class ErrorResponseHandler {
|
||||||
|
@ExceptionHandler
|
||||||
|
public ResponseEntity<?> handle(ResponseStatusException e) {
|
||||||
|
Map<String, Object> responseContent = new HashMap<>(1);
|
||||||
|
String message;
|
||||||
|
if (e.getReason() != null) {
|
||||||
|
message = e.getReason();
|
||||||
|
} else {
|
||||||
|
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
|
||||||
|
message = "Resource not found.";
|
||||||
|
} else if (e.getStatusCode() == HttpStatus.FORBIDDEN) {
|
||||||
|
message = "Access to this resource is forbidden.";
|
||||||
|
} else if (e.getStatusCode() == HttpStatus.INTERNAL_SERVER_ERROR) {
|
||||||
|
message = "An internal error occurred. Please try again later.";
|
||||||
|
} else {
|
||||||
|
message = "The request could not be completed.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
responseContent.put("message", message);
|
||||||
|
return ResponseEntity.status(e.getStatusCode()).body(responseContent);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.domains.api.model;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
public enum WeightUnit {
|
||||||
|
KILOGRAMS,
|
||||||
|
POUNDS;
|
||||||
|
|
||||||
|
public static WeightUnit parse(String s) {
|
||||||
|
if (s == null || s.isBlank()) return KILOGRAMS;
|
||||||
|
s = s.strip().toUpperCase();
|
||||||
|
if (s.equals("LB") || s.equals("LBS") || s.equals("POUND") || s.equals("POUNDS")) {
|
||||||
|
return POUNDS;
|
||||||
|
}
|
||||||
|
return KILOGRAMS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BigDecimal toKilograms(BigDecimal pounds) {
|
||||||
|
BigDecimal metric = new BigDecimal("0.45359237");
|
||||||
|
return metric.multiply(pounds);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package nl.andrewlalis.gymboard_api.domains.api.model.exercise;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.model.WeightUnit;
|
||||||
import org.hibernate.annotations.CreationTimestamp;
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
@ -10,11 +11,6 @@ import java.time.LocalDateTime;
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "exercise_submission")
|
@Table(name = "exercise_submission")
|
||||||
public class ExerciseSubmission {
|
public class ExerciseSubmission {
|
||||||
public enum WeightUnit {
|
|
||||||
KG,
|
|
||||||
LBS
|
|
||||||
}
|
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@Column(nullable = false, updatable = false, length = 26)
|
@Column(nullable = false, updatable = false, length = 26)
|
||||||
private String id;
|
private String id;
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
package nl.andrewlalis.gymboard_api.domains.api.service.submission;
|
package nl.andrewlalis.gymboard_api.domains.api.service.submission;
|
||||||
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dto.CompoundGymId;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseSubmissionPayload;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseSubmissionResponse;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dao.GymRepository;
|
import nl.andrewlalis.gymboard_api.domains.api.dao.GymRepository;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseRepository;
|
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseRepository;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseSubmissionRepository;
|
import nl.andrewlalis.gymboard_api.domains.api.dao.exercise.ExerciseSubmissionRepository;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.dto.CompoundGymId;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseSubmissionPayload;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.dto.ExerciseSubmissionResponse;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.model.WeightUnit;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.Exercise;
|
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.Exercise;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.ExerciseSubmission;
|
import nl.andrewlalis.gymboard_api.domains.api.model.exercise.ExerciseSubmission;
|
||||||
import nl.andrewlalis.gymboard_api.util.ULID;
|
import nl.andrewlalis.gymboard_api.util.ULID;
|
||||||
|
@ -66,10 +67,10 @@ public class ExerciseSubmissionService {
|
||||||
|
|
||||||
// Create the submission.
|
// Create the submission.
|
||||||
BigDecimal rawWeight = BigDecimal.valueOf(payload.weight());
|
BigDecimal rawWeight = BigDecimal.valueOf(payload.weight());
|
||||||
ExerciseSubmission.WeightUnit unit = ExerciseSubmission.WeightUnit.valueOf(payload.weightUnit().toUpperCase());
|
WeightUnit weightUnit = WeightUnit.parse(payload.weightUnit());
|
||||||
BigDecimal metricWeight = BigDecimal.valueOf(payload.weight());
|
BigDecimal metricWeight = BigDecimal.valueOf(payload.weight());
|
||||||
if (unit == ExerciseSubmission.WeightUnit.LBS) {
|
if (weightUnit == WeightUnit.POUNDS) {
|
||||||
metricWeight = metricWeight.multiply(new BigDecimal("0.45359237"));
|
metricWeight = WeightUnit.toKilograms(rawWeight);
|
||||||
}
|
}
|
||||||
ExerciseSubmission submission = exerciseSubmissionRepository.saveAndFlush(new ExerciseSubmission(
|
ExerciseSubmission submission = exerciseSubmissionRepository.saveAndFlush(new ExerciseSubmission(
|
||||||
ulid.nextULID(),
|
ulid.nextULID(),
|
||||||
|
@ -78,7 +79,7 @@ public class ExerciseSubmissionService {
|
||||||
payload.videoFileId(),
|
payload.videoFileId(),
|
||||||
payload.name(),
|
payload.name(),
|
||||||
rawWeight,
|
rawWeight,
|
||||||
unit,
|
weightUnit,
|
||||||
metricWeight,
|
metricWeight,
|
||||||
payload.reps()
|
payload.reps()
|
||||||
));
|
));
|
||||||
|
|
|
@ -2,11 +2,16 @@ package nl.andrewlalis.gymboard_api.domains.auth.dao;
|
||||||
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.auth.model.UserActivationCode;
|
import nl.andrewlalis.gymboard_api.domains.auth.model.UserActivationCode;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface UserActivationCodeRepository extends JpaRepository<UserActivationCode, Long> {
|
public interface UserActivationCodeRepository extends JpaRepository<UserActivationCode, Long> {
|
||||||
Optional<UserActivationCode> findByCode(String code);
|
Optional<UserActivationCode> findByCode(String code);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
void deleteAllByCreatedAtBefore(LocalDateTime cutoff);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,14 @@ package nl.andrewlalis.gymboard_api.domains.auth.model;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import org.hibernate.annotations.CreationTimestamp;
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "auth_user_password_reset_code")
|
@Table(name = "auth_user_password_reset_code")
|
||||||
public class PasswordResetCode {
|
public class PasswordResetCode {
|
||||||
|
public static final Duration VALID_FOR = Duration.ofMinutes(30);
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@Column(nullable = false, updatable = false, length = 127)
|
@Column(nullable = false, updatable = false, length = 127)
|
||||||
private String code;
|
private String code;
|
||||||
|
|
|
@ -37,6 +37,9 @@ public class User {
|
||||||
)
|
)
|
||||||
private Set<Role> roles;
|
private Set<Role> roles;
|
||||||
|
|
||||||
|
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, optional = false)
|
||||||
|
private UserPersonalDetails personalDetails;
|
||||||
|
|
||||||
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) {
|
||||||
|
|
|
@ -3,11 +3,14 @@ package nl.andrewlalis.gymboard_api.domains.auth.model;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import org.hibernate.annotations.CreationTimestamp;
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "auth_user_activation_code")
|
@Table(name = "auth_user_activation_code")
|
||||||
public class UserActivationCode {
|
public class UserActivationCode {
|
||||||
|
public static final Duration VALID_FOR = Duration.ofHours(24);
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.domains.auth.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores information about a "following" relationship between users, which can
|
||||||
|
* have impacts on the notifications that a user can receive.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "auth_user_following")
|
||||||
|
public class UserFollowing {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
|
private User followedUser;
|
||||||
|
|
||||||
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
|
private User followingUser;
|
||||||
|
|
||||||
|
public UserFollowing() {}
|
||||||
|
|
||||||
|
public UserFollowing(User followedUser, User followingUser) {
|
||||||
|
this.followedUser = followedUser;
|
||||||
|
this.followingUser = followingUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getFollowedUser() {
|
||||||
|
return followedUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getFollowingUser() {
|
||||||
|
return followingUser;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.domains.auth.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.model.WeightUnit;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Personal details that belong to a user.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "auth_user_personal_details")
|
||||||
|
public class UserPersonalDetails {
|
||||||
|
public enum PersonSex {
|
||||||
|
MALE,
|
||||||
|
FEMALE,
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private LocalDate birthDate;
|
||||||
|
|
||||||
|
@Column(precision = 7, scale = 2)
|
||||||
|
private BigDecimal currentWeight;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private WeightUnit currentWeightUnit;
|
||||||
|
|
||||||
|
@Column(precision = 7, scale = 2)
|
||||||
|
private BigDecimal currentMetricWeight;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private PersonSex sex = PersonSex.UNKNOWN;
|
||||||
|
|
||||||
|
public UserPersonalDetails() {}
|
||||||
|
|
||||||
|
public UserPersonalDetails(User user) {
|
||||||
|
this.user = user;
|
||||||
|
this.userId = user.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getBirthDate() {
|
||||||
|
return birthDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getCurrentWeight() {
|
||||||
|
return currentWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WeightUnit getCurrentWeightUnit() {
|
||||||
|
return currentWeightUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getCurrentMetricWeight() {
|
||||||
|
return currentMetricWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PersonSex getSex() {
|
||||||
|
return sex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBirthDate(LocalDate birthDate) {
|
||||||
|
this.birthDate = birthDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentWeight(BigDecimal currentWeight) {
|
||||||
|
this.currentWeight = currentWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentWeightUnit(WeightUnit currentWeightUnit) {
|
||||||
|
this.currentWeightUnit = currentWeightUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentMetricWeight(BigDecimal currentMetricWeight) {
|
||||||
|
this.currentMetricWeight = currentMetricWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSex(PersonSex sex) {
|
||||||
|
this.sex = sex;
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ 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.JavaMailSender;
|
||||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
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;
|
||||||
|
@ -25,6 +26,7 @@ import org.springframework.web.server.ResponseStatusException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class UserService {
|
public class UserService {
|
||||||
|
@ -124,7 +126,7 @@ public class UserService {
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
|
||||||
User user = activationCode.getUser();
|
User user = activationCode.getUser();
|
||||||
if (!user.isActivated()) {
|
if (!user.isActivated()) {
|
||||||
LocalDateTime cutoff = LocalDateTime.now().minusDays(1);
|
LocalDateTime cutoff = LocalDateTime.now().minus(UserActivationCode.VALID_FOR);
|
||||||
if (activationCode.getCreatedAt().isBefore(cutoff)) {
|
if (activationCode.getCreatedAt().isBefore(cutoff)) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Code is expired.");
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Code is expired.");
|
||||||
}
|
}
|
||||||
|
@ -182,7 +184,7 @@ public class UserService {
|
||||||
public void resetUserPassword(PasswordResetPayload payload) {
|
public void resetUserPassword(PasswordResetPayload payload) {
|
||||||
PasswordResetCode code = passwordResetCodeRepository.findById(payload.code())
|
PasswordResetCode code = passwordResetCodeRepository.findById(payload.code())
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
LocalDateTime cutoff = LocalDateTime.now().minusMinutes(30);
|
LocalDateTime cutoff = LocalDateTime.now().minus(PasswordResetCode.VALID_FOR);
|
||||||
if (code.getCreatedAt().isBefore(cutoff)) {
|
if (code.getCreatedAt().isBefore(cutoff)) {
|
||||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
@ -205,4 +207,17 @@ public class UserService {
|
||||||
user.setPasswordHash(passwordEncoder.encode(payload.newPassword()));
|
user.setPasswordHash(passwordEncoder.encode(payload.newPassword()));
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scheduled task that periodically removes all old authentication entities
|
||||||
|
* so that they don't clutter up the system.
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedDelay = 1, timeUnit = TimeUnit.HOURS)
|
||||||
|
@Transactional
|
||||||
|
public void removeOldAuthEntities() {
|
||||||
|
LocalDateTime passwordResetCodeCutoff = LocalDateTime.now().minus(PasswordResetCode.VALID_FOR);
|
||||||
|
passwordResetCodeRepository.deleteAllByCreatedAtBefore(passwordResetCodeCutoff);
|
||||||
|
LocalDateTime activationCodeCutoff = LocalDateTime.now().minus(UserActivationCode.VALID_FOR);
|
||||||
|
activationCodeRepository.deleteAllByCreatedAtBefore(activationCodeCutoff);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue