Added email reset code and follow request.
This commit is contained in:
parent
bc1e2b4397
commit
3653fe697e
|
@ -5,7 +5,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>3.0.2</version>
|
<version>3.0.5</version>
|
||||||
<relativePath/> <!-- lookup parent from repository -->
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
</parent>
|
</parent>
|
||||||
<groupId>nl.andrewlalis</groupId>
|
<groupId>nl.andrewlalis</groupId>
|
||||||
|
@ -51,25 +51,25 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-csv</artifactId>
|
<artifactId>commons-csv</artifactId>
|
||||||
<version>1.9.0</version>
|
<version>1.10.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- JWT dependencies -->
|
<!-- JWT dependencies -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
<artifactId>jjwt-api</artifactId>
|
<artifactId>jjwt-api</artifactId>
|
||||||
<version>0.11.2</version>
|
<version>0.11.5</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
<artifactId>jjwt-impl</artifactId>
|
<artifactId>jjwt-impl</artifactId>
|
||||||
<version>0.11.2</version>
|
<version>0.11.5</version>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
|
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
|
||||||
<version>0.11.2</version>
|
<version>0.11.5</version>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mockito</groupId>
|
<groupId>org.mockito</groupId>
|
||||||
<artifactId>mockito-core</artifactId>
|
<artifactId>mockito-core</artifactId>
|
||||||
<version>5.1.1</version>
|
<version>5.2.0</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
|
<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
|
||||||
|
|
|
@ -15,6 +15,11 @@ import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A filter that performs token authentication on incoming requests, and if a
|
||||||
|
* user's token is valid, sets the security context's authentication to a new
|
||||||
|
* instance of {@link TokenAuthentication}.
|
||||||
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
||||||
private final TokenService tokenService;
|
private final TokenService tokenService;
|
||||||
|
|
|
@ -53,6 +53,18 @@ public class UserController {
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping(path = "/auth/me/email-reset-code")
|
||||||
|
public ResponseEntity<Void> generateEmailResetCode(@AuthenticationPrincipal User user, @RequestBody EmailUpdatePayload payload) {
|
||||||
|
userService.generateEmailResetCode(user.getId(), payload);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(path = "/auth/me/email")
|
||||||
|
public ResponseEntity<Void> updateMyEmail(@AuthenticationPrincipal User user, @RequestParam String code) {
|
||||||
|
userService.updateEmail(user.getId(), code);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/auth/me/personal-details")
|
@GetMapping(path = "/auth/me/personal-details")
|
||||||
public UserPersonalDetailsResponse getMyPersonalDetails(@AuthenticationPrincipal User user) {
|
public UserPersonalDetailsResponse getMyPersonalDetails(@AuthenticationPrincipal User user) {
|
||||||
return userService.getPersonalDetails(user.getId());
|
return userService.getPersonalDetails(user.getId());
|
||||||
|
@ -85,9 +97,8 @@ public class UserController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(path = "/auth/users/{userId}/followers")
|
@PostMapping(path = "/auth/users/{userId}/followers")
|
||||||
public ResponseEntity<Void> followUser(@AuthenticationPrincipal User myUser, @PathVariable String userId) {
|
public UserFollowResponse followUser(@AuthenticationPrincipal User myUser, @PathVariable String userId) {
|
||||||
userService.followUser(myUser.getId(), userId);
|
return userService.followUser(myUser.getId(), userId);
|
||||||
return ResponseEntity.ok().build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping(path = "/auth/users/{userId}/followers")
|
@DeleteMapping(path = "/auth/users/{userId}/followers")
|
||||||
|
@ -96,13 +107,23 @@ public class UserController {
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping(path = "/auth/me/follow-requests/{followRequestId}")
|
||||||
|
public ResponseEntity<Void> respondToFollowRequest(
|
||||||
|
@AuthenticationPrincipal User myUser,
|
||||||
|
@PathVariable long followRequestId,
|
||||||
|
@RequestBody UserFollowRequestApproval payload
|
||||||
|
) {
|
||||||
|
userService.respondToFollowRequest(myUser.getId(), followRequestId, payload.approve());
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/auth/me/followers")
|
@GetMapping(path = "/auth/me/followers")
|
||||||
public Page<UserResponse> getFollowers(@AuthenticationPrincipal User user, Pageable pageable) {
|
public Page<UserResponse> getMyFollowers(@AuthenticationPrincipal User user, Pageable pageable) {
|
||||||
return userService.getFollowers(user.getId(), pageable);
|
return userService.getFollowers(user.getId(), pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = "/auth/me/following")
|
@GetMapping(path = "/auth/me/following")
|
||||||
public Page<UserResponse> getFollowing(@AuthenticationPrincipal User user, Pageable pageable) {
|
public Page<UserResponse> getMyFollowing(@AuthenticationPrincipal User user, Pageable pageable) {
|
||||||
return userService.getFollowing(user.getId(), pageable);
|
return userService.getFollowing(user.getId(), pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.domains.auth.dao;
|
||||||
|
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.auth.model.EmailResetCode;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface EmailResetCodeRepository extends JpaRepository<EmailResetCode, String> {
|
||||||
|
@Modifying
|
||||||
|
void deleteAllByCreatedAtBefore(LocalDateTime cutoff);
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.domains.auth.dao;
|
||||||
|
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.auth.model.UserFollowRequest;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface UserFollowRequestRepository extends JpaRepository<UserFollowRequest, Long> {
|
||||||
|
@Modifying
|
||||||
|
void deleteAllByCreatedAtBefore(LocalDateTime cutoff);
|
||||||
|
}
|
|
@ -17,4 +17,7 @@ public interface UserFollowingRepository extends JpaRepository<UserFollowing, Lo
|
||||||
|
|
||||||
Page<UserFollowing> findAllByFollowedUserOrderByCreatedAtDesc(User followedUser, Pageable pageable);
|
Page<UserFollowing> findAllByFollowedUserOrderByCreatedAtDesc(User followedUser, Pageable pageable);
|
||||||
Page<UserFollowing> findAllByFollowingUserOrderByCreatedAtDesc(User followingUser, Pageable pageable);
|
Page<UserFollowing> findAllByFollowingUserOrderByCreatedAtDesc(User followingUser, Pageable pageable);
|
||||||
|
|
||||||
|
long countByFollowedUser(User followedUser);
|
||||||
|
long countByFollowingUser(User followingUser);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.domains.auth.dto;
|
||||||
|
|
||||||
|
public record EmailUpdatePayload(String newEmail) {}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.domains.auth.dto;
|
||||||
|
|
||||||
|
public record UserFollowRequestApproval(boolean approve) {}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.domains.auth.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A response that's sent when a user requests to follow another.
|
||||||
|
*/
|
||||||
|
public record UserFollowResponse(
|
||||||
|
String result
|
||||||
|
) {
|
||||||
|
private static final String RESULT_FOLLOWED = "FOLLOWED";
|
||||||
|
private static final String RESULT_REQUESTED = "REQUESTED";
|
||||||
|
private static final String RESULT_ALREADY_FOLLOWED = "ALREADY_FOLLOWED";
|
||||||
|
|
||||||
|
public static UserFollowResponse followed() {
|
||||||
|
return new UserFollowResponse(RESULT_FOLLOWED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UserFollowResponse requested() {
|
||||||
|
return new UserFollowResponse(RESULT_REQUESTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UserFollowResponse alreadyFollowed() {
|
||||||
|
return new UserFollowResponse(RESULT_ALREADY_FOLLOWED);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.domains.auth.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A code that's sent to a user's new email address to confirm that they own
|
||||||
|
* it. Once confirmed, the user's email address will be updated.
|
||||||
|
*/
|
||||||
|
@Table(name = "auth_email_reset_code")
|
||||||
|
@Entity
|
||||||
|
public class EmailResetCode {
|
||||||
|
public static final Duration VALID_FOR = Duration.ofMinutes(30);
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(nullable = false, updatable = false, length = 127)
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
@Column(nullable = false, updatable = false, unique = true)
|
||||||
|
private String newEmail;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
public EmailResetCode() {}
|
||||||
|
|
||||||
|
public EmailResetCode(String code, String newEmail, User user) {
|
||||||
|
this.code = code;
|
||||||
|
this.newEmail = newEmail;
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNewEmail() {
|
||||||
|
return newEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,10 @@ import org.hibernate.annotations.CreationTimestamp;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A code that's sent to a user's email address to grant them access to change
|
||||||
|
* their password without needing to log in.
|
||||||
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "auth_user_password_reset_code")
|
@Table(name = "auth_user_password_reset_code")
|
||||||
public class PasswordResetCode {
|
public class PasswordResetCode {
|
||||||
|
|
|
@ -6,6 +6,13 @@ import org.springframework.security.core.GrantedAuthority;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The authentication instance that's used to represent a user who has
|
||||||
|
* authenticated with an API token (so most users).
|
||||||
|
* @param user The user who authenticated. The user entity has its roles eagerly
|
||||||
|
* loaded.
|
||||||
|
* @param token The token that was used.
|
||||||
|
*/
|
||||||
public record TokenAuthentication(
|
public record TokenAuthentication(
|
||||||
User user,
|
User user,
|
||||||
String token
|
String token
|
||||||
|
|
|
@ -76,6 +76,10 @@ public class User {
|
||||||
return email;
|
return email;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setEmail(String email) {
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
public String getPasswordHash() {
|
public String getPasswordHash() {
|
||||||
return passwordHash;
|
return passwordHash;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.domains.auth.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A request that one user sends to ask to follow another user.
|
||||||
|
*/
|
||||||
|
@Table(
|
||||||
|
name = "auth_user_follow_request",
|
||||||
|
uniqueConstraints = @UniqueConstraint(columnNames = {"requesting_user_id", "user_to_follow_id"})
|
||||||
|
)
|
||||||
|
@Entity
|
||||||
|
public class UserFollowRequest {
|
||||||
|
public static final Duration VALID_FOR = Duration.ofDays(7);
|
||||||
|
|
||||||
|
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
|
private User requestingUser;
|
||||||
|
|
||||||
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
|
private User userToFollow;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private Boolean approved;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private LocalDateTime decidedAt;
|
||||||
|
|
||||||
|
public UserFollowRequest() {}
|
||||||
|
|
||||||
|
public UserFollowRequest(User requestingUser, User userToFollow) {
|
||||||
|
this.requestingUser = requestingUser;
|
||||||
|
this.userToFollow = userToFollow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getRequestingUser() {
|
||||||
|
return requestingUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getUserToFollow() {
|
||||||
|
return userToFollow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getApproved() {
|
||||||
|
return approved;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getDecidedAt() {
|
||||||
|
return decidedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setApproved(boolean approved) {
|
||||||
|
this.approved = approved;
|
||||||
|
this.decidedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,10 @@ import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A user report is submitted by one user to report inappropriate actions of
|
||||||
|
* another user.
|
||||||
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "auth_user_report")
|
@Table(name = "auth_user_report")
|
||||||
public class UserReport {
|
public class UserReport {
|
||||||
|
|
|
@ -28,6 +28,8 @@ import java.time.LocalDateTime;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static nl.andrewlalis.gymboard_api.util.DataUtils.findByIdOrThrow;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class UserService {
|
public class UserService {
|
||||||
private static final Logger log = LoggerFactory.getLogger(UserService.class);
|
private static final Logger log = LoggerFactory.getLogger(UserService.class);
|
||||||
|
@ -37,7 +39,9 @@ public class UserService {
|
||||||
private final UserPreferencesRepository userPreferencesRepository;
|
private final UserPreferencesRepository userPreferencesRepository;
|
||||||
private final UserActivationCodeRepository activationCodeRepository;
|
private final UserActivationCodeRepository activationCodeRepository;
|
||||||
private final PasswordResetCodeRepository passwordResetCodeRepository;
|
private final PasswordResetCodeRepository passwordResetCodeRepository;
|
||||||
|
private final EmailResetCodeRepository emailResetCodeRepository;
|
||||||
private final UserFollowingRepository userFollowingRepository;
|
private final UserFollowingRepository userFollowingRepository;
|
||||||
|
private final UserFollowRequestRepository followRequestRepository;
|
||||||
private final UserAccessService userAccessService;
|
private final UserAccessService userAccessService;
|
||||||
private final ULID ulid;
|
private final ULID ulid;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
@ -52,7 +56,10 @@ public class UserService {
|
||||||
UserPreferencesRepository userPreferencesRepository,
|
UserPreferencesRepository userPreferencesRepository,
|
||||||
UserActivationCodeRepository activationCodeRepository,
|
UserActivationCodeRepository activationCodeRepository,
|
||||||
PasswordResetCodeRepository passwordResetCodeRepository,
|
PasswordResetCodeRepository passwordResetCodeRepository,
|
||||||
UserFollowingRepository userFollowingRepository, UserAccessService userAccessService, ULID ulid,
|
EmailResetCodeRepository emailResetCodeRepository, UserFollowingRepository userFollowingRepository,
|
||||||
|
UserFollowRequestRepository followRequestRepository,
|
||||||
|
UserAccessService userAccessService,
|
||||||
|
ULID ulid,
|
||||||
PasswordEncoder passwordEncoder,
|
PasswordEncoder passwordEncoder,
|
||||||
JavaMailSender mailSender
|
JavaMailSender mailSender
|
||||||
) {
|
) {
|
||||||
|
@ -61,7 +68,9 @@ public class UserService {
|
||||||
this.userPreferencesRepository = userPreferencesRepository;
|
this.userPreferencesRepository = userPreferencesRepository;
|
||||||
this.activationCodeRepository = activationCodeRepository;
|
this.activationCodeRepository = activationCodeRepository;
|
||||||
this.passwordResetCodeRepository = passwordResetCodeRepository;
|
this.passwordResetCodeRepository = passwordResetCodeRepository;
|
||||||
|
this.emailResetCodeRepository = emailResetCodeRepository;
|
||||||
this.userFollowingRepository = userFollowingRepository;
|
this.userFollowingRepository = userFollowingRepository;
|
||||||
|
this.followRequestRepository = followRequestRepository;
|
||||||
this.userAccessService = userAccessService;
|
this.userAccessService = userAccessService;
|
||||||
this.ulid = ulid;
|
this.ulid = ulid;
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
@ -218,6 +227,59 @@ public class UserService {
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void generateEmailResetCode(String id, EmailUpdatePayload payload) {
|
||||||
|
User user = findByIdOrThrow(id, userRepository);
|
||||||
|
if (userRepository.existsByEmail(payload.newEmail())) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Email is taken.");
|
||||||
|
}
|
||||||
|
EmailResetCode emailResetCode = emailResetCodeRepository.save(new EmailResetCode(
|
||||||
|
StringGenerator.randomString(127, StringGenerator.Alphabet.ALPHANUMERIC),
|
||||||
|
payload.newEmail(),
|
||||||
|
user
|
||||||
|
));
|
||||||
|
String emailContent = String.format(
|
||||||
|
"""
|
||||||
|
<p>Hello %s,</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
You've just requested to change your email from %s to this email address.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Please click enter this code to reset your email: %s
|
||||||
|
</p>
|
||||||
|
""",
|
||||||
|
user.getName(),
|
||||||
|
user.getEmail(),
|
||||||
|
emailResetCode.getCode()
|
||||||
|
);
|
||||||
|
MimeMessage msg = mailSender.createMimeMessage();
|
||||||
|
try {
|
||||||
|
MimeMessageHelper helper = new MimeMessageHelper(msg, "UTF-8");
|
||||||
|
helper.setFrom("Gymboard <noreply@gymboard.io>");
|
||||||
|
helper.setSubject("Gymboard Account Email Update");
|
||||||
|
helper.setTo(emailResetCode.getNewEmail());
|
||||||
|
helper.setText(emailContent, true);
|
||||||
|
mailSender.send(msg);
|
||||||
|
} catch (MessagingException e) {
|
||||||
|
log.error("Error sending user email update email.", e);
|
||||||
|
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void updateEmail(String userId, String code) {
|
||||||
|
User user = findByIdOrThrow(userId, userRepository);
|
||||||
|
EmailResetCode emailResetCode = findByIdOrThrow(code, emailResetCodeRepository);
|
||||||
|
if (!emailResetCode.getUser().getId().equals(user.getId())) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
user.setEmail(emailResetCode.getNewEmail());
|
||||||
|
userRepository.save(user);
|
||||||
|
emailResetCodeRepository.delete(emailResetCode);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scheduled task that periodically removes all old authentication entities
|
* Scheduled task that periodically removes all old authentication entities
|
||||||
* so that they don't clutter up the system.
|
* so that they don't clutter up the system.
|
||||||
|
@ -229,6 +291,10 @@ public class UserService {
|
||||||
passwordResetCodeRepository.deleteAllByCreatedAtBefore(passwordResetCodeCutoff);
|
passwordResetCodeRepository.deleteAllByCreatedAtBefore(passwordResetCodeCutoff);
|
||||||
LocalDateTime activationCodeCutoff = LocalDateTime.now().minus(UserActivationCode.VALID_FOR);
|
LocalDateTime activationCodeCutoff = LocalDateTime.now().minus(UserActivationCode.VALID_FOR);
|
||||||
activationCodeRepository.deleteAllByCreatedAtBefore(activationCodeCutoff);
|
activationCodeRepository.deleteAllByCreatedAtBefore(activationCodeCutoff);
|
||||||
|
LocalDateTime followRequestCutoff = LocalDateTime.now().minus(UserFollowRequest.VALID_FOR);
|
||||||
|
followRequestRepository.deleteAllByCreatedAtBefore(followRequestCutoff);
|
||||||
|
LocalDateTime emailResetCodeCutoff = LocalDateTime.now().minus(EmailResetCode.VALID_FOR);
|
||||||
|
emailResetCodeRepository.deleteAllByCreatedAtBefore(emailResetCodeCutoff);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
@ -280,30 +346,64 @@ public class UserService {
|
||||||
return new UserPreferencesResponse(p);
|
return new UserPreferencesResponse(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a user indicates that they'd like to follow another, this method is
|
||||||
|
* invoked. If the person they want to follow is private, we create a new
|
||||||
|
* {@link UserFollowRequest} that the person must approve. Otherwise, the
|
||||||
|
* user just starts following the person. A 400 bad request is thrown if the
|
||||||
|
* user tries to follow themselves.
|
||||||
|
* @param followerId The id of the user that's trying to follow a user.
|
||||||
|
* @param followedId The id of the user that's being followed.
|
||||||
|
* @return A response that indicates the outcome.
|
||||||
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public void followUser(String followerId, String followedId) {
|
public UserFollowResponse followUser(String followerId, String followedId) {
|
||||||
if (followerId.equals(followedId)) return;
|
if (followerId.equals(followedId)) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "You can't follow yourself.");
|
||||||
User follower = userRepository.findById(followerId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
User follower = findByIdOrThrow(followerId, userRepository);
|
||||||
User followed = userRepository.findById(followedId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
User followed = findByIdOrThrow(followedId, userRepository);
|
||||||
|
|
||||||
if (!userFollowingRepository.existsByFollowedUserAndFollowingUser(followed, follower)) {
|
if (!userFollowingRepository.existsByFollowedUserAndFollowingUser(followed, follower)) {
|
||||||
userFollowingRepository.save(new UserFollowing(followed, follower));
|
if (followed.getPreferences().isAccountPrivate()) {
|
||||||
|
userFollowingRepository.save(new UserFollowing(followed, follower));
|
||||||
|
return UserFollowResponse.requested();
|
||||||
|
} else {
|
||||||
|
followRequestRepository.save(new UserFollowRequest(follower, followed));
|
||||||
|
return UserFollowResponse.followed();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return UserFollowResponse.alreadyFollowed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void unfollowUser(String followerId, String followedId) {
|
public void unfollowUser(String followerId, String followedId) {
|
||||||
if (followerId.equals(followedId)) return;
|
if (followerId.equals(followedId)) return;
|
||||||
User follower = userRepository.findById(followerId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
User follower = findByIdOrThrow(followerId, userRepository);
|
||||||
User followed = userRepository.findById(followedId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
User followed = findByIdOrThrow(followedId, userRepository);
|
||||||
|
|
||||||
userFollowingRepository.deleteByFollowedUserAndFollowingUser(followed, follower);
|
userFollowingRepository.deleteByFollowedUserAndFollowingUser(followed, follower);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void respondToFollowRequest(String userId, long followRequestId, boolean approved) {
|
||||||
|
User followedUser = findByIdOrThrow(userId, userRepository);
|
||||||
|
UserFollowRequest followRequest = findByIdOrThrow(followRequestId, followRequestRepository);
|
||||||
|
if (!followRequest.getUserToFollow().getId().equals(followedUser.getId())) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (followRequest.getApproved() != null) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Request already decided.");
|
||||||
|
}
|
||||||
|
followRequest.setApproved(approved);
|
||||||
|
followRequestRepository.save(followRequest);
|
||||||
|
if (approved) {
|
||||||
|
userFollowingRepository.save(new UserFollowing(followedUser, followRequest.getRequestingUser()));
|
||||||
|
// TODO: Send notification to the user who requested to follow.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Page<UserResponse> getFollowers(String userId, Pageable pageable) {
|
public Page<UserResponse> getFollowers(String userId, Pageable pageable) {
|
||||||
User user = userRepository.findById(userId)
|
User user = findByIdOrThrow(userId, userRepository);
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
|
||||||
userAccessService.enforceUserAccess(user);
|
userAccessService.enforceUserAccess(user);
|
||||||
return userFollowingRepository.findAllByFollowedUserOrderByCreatedAtDesc(user, pageable)
|
return userFollowingRepository.findAllByFollowedUserOrderByCreatedAtDesc(user, pageable)
|
||||||
.map(UserFollowing::getFollowingUser)
|
.map(UserFollowing::getFollowingUser)
|
||||||
|
@ -312,20 +412,25 @@ public class UserService {
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Page<UserResponse> getFollowing(String userId, Pageable pageable) {
|
public Page<UserResponse> getFollowing(String userId, Pageable pageable) {
|
||||||
User user = userRepository.findById(userId)
|
User user = findByIdOrThrow(userId, userRepository);
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
|
||||||
userAccessService.enforceUserAccess(user);
|
userAccessService.enforceUserAccess(user);
|
||||||
return userFollowingRepository.findAllByFollowingUserOrderByCreatedAtDesc(user, pageable)
|
return userFollowingRepository.findAllByFollowingUserOrderByCreatedAtDesc(user, pageable)
|
||||||
.map(UserFollowing::getFollowedUser)
|
.map(UserFollowing::getFollowedUser)
|
||||||
.map(UserResponse::new);
|
.map(UserResponse::new);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getFollowerCount(String userId) {
|
||||||
|
return userFollowingRepository.countByFollowedUser(findByIdOrThrow(userId, userRepository));
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getFollowingCount(String userId) {
|
||||||
|
return userFollowingRepository.countByFollowingUser(findByIdOrThrow(userId, userRepository));
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public UserRelationshipResponse getRelationship(String user1Id, String user2Id) {
|
public UserRelationshipResponse getRelationship(String user1Id, String user2Id) {
|
||||||
User user1 = userRepository.findById(user1Id)
|
User user1 = findByIdOrThrow(user1Id, userRepository);
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
User user2 = findByIdOrThrow(user2Id, userRepository);
|
||||||
User user2 = userRepository.findById(user2Id)
|
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
|
||||||
userAccessService.enforceUserAccess(user1);
|
userAccessService.enforceUserAccess(user1);
|
||||||
boolean user1FollowingUser2 = userFollowingRepository.existsByFollowedUserAndFollowingUser(user2, user1);
|
boolean user1FollowingUser2 = userFollowingRepository.existsByFollowedUserAndFollowingUser(user2, user1);
|
||||||
boolean user1FollowedByUser2 = userFollowingRepository.existsByFollowedUserAndFollowingUser(user1, user2);
|
boolean user1FollowedByUser2 = userFollowingRepository.existsByFollowedUserAndFollowingUser(user1, user2);
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package nl.andrewlalis.gymboard_api.util;
|
||||||
|
|
||||||
|
import org.springframework.data.repository.CrudRepository;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
public class DataUtils {
|
||||||
|
/**
|
||||||
|
* Finds an entity by its id, or throws a 404 not found exception.
|
||||||
|
* @param id The id to look for.
|
||||||
|
* @param repo The repository to search in.
|
||||||
|
* @return The entity that was found.
|
||||||
|
* @param <T> The entity type.
|
||||||
|
* @param <ID> The id type.
|
||||||
|
* @throws ResponseStatusException If the entity wasn't found.
|
||||||
|
*/
|
||||||
|
public static <T, ID> T findByIdOrThrow(ID id, CrudRepository<T, ID> repo) throws ResponseStatusException {
|
||||||
|
return repo.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue