Added email reset code and follow request.

This commit is contained in:
Andrew Lalis 2023-03-24 16:21:03 +01:00
parent bc1e2b4397
commit 3653fe697e
17 changed files with 385 additions and 27 deletions

View File

@ -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 -->

View File

@ -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;

View File

@ -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);
} }

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
} }

View File

@ -0,0 +1,3 @@
package nl.andrewlalis.gymboard_api.domains.auth.dto;
public record EmailUpdatePayload(String newEmail) {}

View File

@ -0,0 +1,3 @@
package nl.andrewlalis.gymboard_api.domains.auth.dto;
public record UserFollowRequestApproval(boolean approve) {}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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;
} }

View File

@ -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();
}
}

View File

@ -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 {

View File

@ -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);

View File

@ -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));
}
}