feat: like and unlike backend endpoints

This commit is contained in:
sBubshait 2025-07-23 14:16:08 +03:00
parent 86a4535f9a
commit 066e032120
5 changed files with 237 additions and 0 deletions

View File

@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import online.wesal.wesal.dto.ApiResponse;
import online.wesal.wesal.dto.PostCreateRequestDTO;
import online.wesal.wesal.dto.PostLikeRequestDTO;
import online.wesal.wesal.dto.PostResponseDTO;
import online.wesal.wesal.entity.Post;
import online.wesal.wesal.service.PostService;
@ -109,4 +110,78 @@ public class PostController {
return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later"));
}
}
@PostMapping(value = "/like", consumes = "application/json", produces = "application/json")
@Operation(summary = "Like post", description = "Like a post. Returns the updated post with new like count.")
public ResponseEntity<ApiResponse<PostResponseDTO>> likePost(
@Valid @RequestBody PostLikeRequestDTO request,
BindingResult bindingResult,
Authentication authentication) {
if (bindingResult.hasErrors()) {
String errorMessage = bindingResult.getFieldErrors().stream()
.map(error -> {
String field = error.getField();
if ("postId".equals(field)) return "Valid post ID is required";
return error.getDefaultMessage();
})
.findFirst()
.orElse("Invalid input");
return ResponseEntity.badRequest().body(ApiResponse.error(errorMessage));
}
try {
PostResponseDTO response = postService.likePost(request.getPostId());
return ResponseEntity.ok(ApiResponse.success(response));
} catch (RuntimeException e) {
String message;
if (e.getMessage().contains("Post not found")) {
message = "Post not found";
} else if (e.getMessage().contains("User not found")) {
message = "Authentication error. Please log in again.";
} else {
message = "Something went wrong.. We're sorry but try again later";
}
return ResponseEntity.badRequest().body(ApiResponse.error(message));
} catch (Exception e) {
return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later"));
}
}
@PostMapping(value = "/unlike", consumes = "application/json", produces = "application/json")
@Operation(summary = "Unlike post", description = "Unlike a post. Returns the updated post with new like count.")
public ResponseEntity<ApiResponse<PostResponseDTO>> unlikePost(
@Valid @RequestBody PostLikeRequestDTO request,
BindingResult bindingResult,
Authentication authentication) {
if (bindingResult.hasErrors()) {
String errorMessage = bindingResult.getFieldErrors().stream()
.map(error -> {
String field = error.getField();
if ("postId".equals(field)) return "Valid post ID is required";
return error.getDefaultMessage();
})
.findFirst()
.orElse("Invalid input");
return ResponseEntity.badRequest().body(ApiResponse.error(errorMessage));
}
try {
PostResponseDTO response = postService.unlikePost(request.getPostId());
return ResponseEntity.ok(ApiResponse.success(response));
} catch (RuntimeException e) {
String message;
if (e.getMessage().contains("Post not found")) {
message = "Post not found";
} else if (e.getMessage().contains("User not found")) {
message = "Authentication error. Please log in again.";
} else {
message = "Something went wrong.. We're sorry but try again later";
}
return ResponseEntity.badRequest().body(ApiResponse.error(message));
} catch (Exception e) {
return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later"));
}
}
}

View File

@ -0,0 +1,25 @@
package online.wesal.wesal.dto;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
public class PostLikeRequestDTO {
@NotNull(message = "Post ID is required")
@Positive(message = "Post ID must be a positive number")
private Long postId;
public PostLikeRequestDTO() {}
public PostLikeRequestDTO(Long postId) {
this.postId = postId;
}
public Long getPostId() {
return postId;
}
public void setPostId(Long postId) {
this.postId = postId;
}
}

View File

@ -0,0 +1,63 @@
package online.wesal.wesal.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "post_likes", uniqueConstraints = {
@UniqueConstraint(columnNames = {"post_id", "user_id"})
})
public class PostLike {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, name = "post_id")
private Long postId;
@Column(nullable = false, name = "user_id")
private Long userId;
@Column(nullable = false)
private LocalDateTime createdAt = LocalDateTime.now();
public PostLike() {}
public PostLike(Long postId, Long userId) {
this.postId = postId;
this.userId = userId;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getPostId() {
return postId;
}
public void setPostId(Long postId) {
this.postId = postId;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
}

View File

@ -0,0 +1,15 @@
package online.wesal.wesal.repository;
import online.wesal.wesal.entity.PostLike;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface PostLikeRepository extends JpaRepository<PostLike, Long> {
Optional<PostLike> findByPostIdAndUserId(Long postId, Long userId);
boolean existsByPostIdAndUserId(Long postId, Long userId);
void deleteByPostIdAndUserId(Long postId, Long userId);
long countByPostId(Long postId);
}

View File

@ -2,11 +2,14 @@ package online.wesal.wesal.service;
import online.wesal.wesal.dto.PostResponseDTO;
import online.wesal.wesal.entity.Post;
import online.wesal.wesal.entity.PostLike;
import online.wesal.wesal.entity.User;
import online.wesal.wesal.repository.PostLikeRepository;
import online.wesal.wesal.repository.PostRepository;
import online.wesal.wesal.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@ -19,6 +22,9 @@ public class PostService {
@Autowired
private PostRepository postRepository;
@Autowired
private PostLikeRepository postLikeRepository;
@Autowired
private UserRepository userRepository;
@ -64,4 +70,57 @@ public class PostService {
post = postRepository.save(post);
return new PostResponseDTO(post, currentUser);
}
@Transactional
public PostResponseDTO likePost(Long postId) {
User currentUser = userService.getCurrentUser();
Long userId = currentUser.getId();
// Check if post exists
Post post = postRepository.findById(postId)
.orElseThrow(() -> new RuntimeException("Post not found"));
// Check if user already liked this post
if (!postLikeRepository.existsByPostIdAndUserId(postId, userId)) {
// Add like record
PostLike postLike = new PostLike(postId, userId);
postLikeRepository.save(postLike);
// Update post likes count
post.setLikes(post.getLikes() + 1);
post = postRepository.save(post);
}
// Get creator for response
User creator = userRepository.findById(post.getCreatorId())
.orElseThrow(() -> new RuntimeException("Creator not found"));
return new PostResponseDTO(post, creator);
}
@Transactional
public PostResponseDTO unlikePost(Long postId) {
User currentUser = userService.getCurrentUser();
Long userId = currentUser.getId();
// Check if post exists
Post post = postRepository.findById(postId)
.orElseThrow(() -> new RuntimeException("Post not found"));
// Check if user has liked this post
if (postLikeRepository.existsByPostIdAndUserId(postId, userId)) {
// Remove like record
postLikeRepository.deleteByPostIdAndUserId(postId, userId);
// Update post likes count
post.setLikes(Math.max(0, post.getLikes() - 1));
post = postRepository.save(post);
}
// Get creator for response
User creator = userRepository.findById(post.getCreatorId())
.orElseThrow(() -> new RuntimeException("Creator not found"));
return new PostResponseDTO(post, creator);
}
}