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 nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
||||
import nl.andrewlalis.gymboard_api.domains.api.model.WeightUnit;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
@ -10,11 +11,6 @@ import java.time.LocalDateTime;
|
|||
@Entity
|
||||
@Table(name = "exercise_submission")
|
||||
public class ExerciseSubmission {
|
||||
public enum WeightUnit {
|
||||
KG,
|
||||
LBS
|
||||
}
|
||||
|
||||
@Id
|
||||
@Column(nullable = false, updatable = false, length = 26)
|
||||
private String id;
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
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.exercise.ExerciseRepository;
|
||||
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.WeightUnit;
|
||||
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.util.ULID;
|
||||
|
@ -66,10 +67,10 @@ public class ExerciseSubmissionService {
|
|||
|
||||
// Create the submission.
|
||||
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());
|
||||
if (unit == ExerciseSubmission.WeightUnit.LBS) {
|
||||
metricWeight = metricWeight.multiply(new BigDecimal("0.45359237"));
|
||||
if (weightUnit == WeightUnit.POUNDS) {
|
||||
metricWeight = WeightUnit.toKilograms(rawWeight);
|
||||
}
|
||||
ExerciseSubmission submission = exerciseSubmissionRepository.saveAndFlush(new ExerciseSubmission(
|
||||
ulid.nextULID(),
|
||||
|
@ -78,7 +79,7 @@ public class ExerciseSubmissionService {
|
|||
payload.videoFileId(),
|
||||
payload.name(),
|
||||
rawWeight,
|
||||
unit,
|
||||
weightUnit,
|
||||
metricWeight,
|
||||
payload.reps()
|
||||
));
|
||||
|
|
|
@ -2,11 +2,16 @@ package nl.andrewlalis.gymboard_api.domains.auth.dao;
|
|||
|
||||
import nl.andrewlalis.gymboard_api.domains.auth.model.UserActivationCode;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface UserActivationCodeRepository extends JpaRepository<UserActivationCode, Long> {
|
||||
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 org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "auth_user_password_reset_code")
|
||||
public class PasswordResetCode {
|
||||
public static final Duration VALID_FOR = Duration.ofMinutes(30);
|
||||
|
||||
@Id
|
||||
@Column(nullable = false, updatable = false, length = 127)
|
||||
private String code;
|
||||
|
|
|
@ -37,6 +37,9 @@ public class User {
|
|||
)
|
||||
private Set<Role> roles;
|
||||
|
||||
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, optional = false)
|
||||
private UserPersonalDetails personalDetails;
|
||||
|
||||
public User() {}
|
||||
|
||||
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 org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "auth_user_activation_code")
|
||||
public class UserActivationCode {
|
||||
public static final Duration VALID_FOR = Duration.ofHours(24);
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
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.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
@ -25,6 +26,7 @@ import org.springframework.web.server.ResponseStatusException;
|
|||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
public class UserService {
|
||||
|
@ -124,7 +126,7 @@ public class UserService {
|
|||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
|
||||
User user = activationCode.getUser();
|
||||
if (!user.isActivated()) {
|
||||
LocalDateTime cutoff = LocalDateTime.now().minusDays(1);
|
||||
LocalDateTime cutoff = LocalDateTime.now().minus(UserActivationCode.VALID_FOR);
|
||||
if (activationCode.getCreatedAt().isBefore(cutoff)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Code is expired.");
|
||||
}
|
||||
|
@ -182,7 +184,7 @@ public class UserService {
|
|||
public void resetUserPassword(PasswordResetPayload payload) {
|
||||
PasswordResetCode code = passwordResetCodeRepository.findById(payload.code())
|
||||
.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)) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
@ -205,4 +207,17 @@ public class UserService {
|
|||
user.setPasswordHash(passwordEncoder.encode(payload.newPassword()));
|
||||
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