Вступление
Добро пожаловать в Часть 6 создания клона Reddit с использованием Spring Boot и React.
Что мы строим в этой части?
- Отправить запрос НА
- Опубликовать ответ НА
- Пользовательские исключения
- Обновленная Служба Авторизации
- Почтовая служба
- ЧТЕНИЕ конечных точек Post
- Создать конечную точку Post
- Обновленное приложение.свойства
В части 5 мы создали логику, необходимую для фильтрации JWT, обновили нашу службу аутентификации и сделали нашу конечную точку subreddit!
Важные ссылки
- Исходный код: https://github.com/MaxiCB/vox-nobis/tree/master/backend
- Источник интерфейса: https://github.com/MaxiCB/vox-nobis/tree/master/client
- Прямой URL-АДРЕС: в процессе
Часть 1: Пост DTO 📨
Давайте рассмотрим наши различные DTO, которые нам понадобятся. Внутри com.your-name.backend.to мы создадим следующие классы.
- Последующий запрос: Обрабатывает создание данных, которые будут отправлены от клиента в API.
package com.maxicb.backend.dto; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @Builder @AllArgsConstructor @NoArgsConstructor public class PostRequest { private Long postId; private String postTitle; private String url; private String description; private String subredditName; }
- PostResponse: Обрабатывает создание данных, которые будут отправлены клиенту из API.
package com.maxicb.backend.dto; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @Builder @AllArgsConstructor @NoArgsConstructor public class PostResponse { private Long postId; private String postTitle; private String url; private String description; private String userName; private String subredditName; private Integer voteCount; private Integer commentCount; private String duration; private boolean upVote; private boolean downVote; }
Часть 2: Пользовательские исключения 🚫
Давайте рассмотрим наши пользовательские исключения, которые нам понадобятся. Внутри com.your-name.backend.exception |/мы создадим следующие классы.
- UserNotFoundException: Обрабатывает исключения, связанные с поиском недопустимого пользователя.
package com.maxicb.backend.exception; public class UserNotFoundException extends RuntimeException { public UserNotFoundException(String message) { super(message); } }
- PostNotFoundException: Обрабатывает исключения, связанные с поиском недопустимой проводки.
package com.maxicb.backend.exception; public class PostNotFoundException extends RuntimeException { public PostNotFoundException(String message) { super(message); } }
Часть 3: Обновленная Служба Авторизации 💂 ♀ ️
Давайте рассмотрим нашу логику проверки JWT, которая нам понадобится. Внутри com.your-name.backend.service мы обновим следующий класс.
- AuthService: Нам нужно реализовать логику, чтобы проверить, вошел ли пользователь в систему в данный момент.
package com.maxicb.backend.service; import com.maxicb.backend.dto.AuthResponse; import com.maxicb.backend.dto.LoginRequest; import com.maxicb.backend.dto.RegisterRequest; import com.maxicb.backend.exception.ActivationException; import com.maxicb.backend.model.AccountVerificationToken; import com.maxicb.backend.model.NotificationEmail; import com.maxicb.backend.model.User; import com.maxicb.backend.repository.TokenRepository; import com.maxicb.backend.repository.UserRepository; import com.maxicb.backend.security.JWTProvider; import lombok.AllArgsConstructor; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.Instant; import java.util.Optional; import java.util.UUID; import static com.maxicb.backend.config.Constants.EMAIL_ACTIVATION; @Service @AllArgsConstructor public class AuthService { UserRepository userRepository; PasswordEncoder passwordEncoder; TokenRepository tokenRepository; MailService mailService; MailBuilder mailBuilder; AuthenticationManager authenticationManager; JWTProvider jwtProvider; @Transactional public void register(RegisterRequest registerRequest) { User user = new User(); user.setUsername(registerRequest.getUsername()); user.setEmail(registerRequest.getEmail()); user.setPassword(encodePassword(registerRequest.getPassword())); user.setCreationDate(Instant.now()); user.setAccountStatus(false); userRepository.save(user); String token = generateToken(user); String message = mailBuilder.build("Welcome to React-Spring-Reddit Clone. " + "Please visit the link below to activate you account : " + EMAIL_ACTIVATION + "/" + token); mailService.sendEmail(new NotificationEmail("Please Activate Your Account", user.getEmail(), message)); } @Transactional(readOnly = true) public User getCurrentUser() { org.springframework.security.core.userdetails.User principal = (org.springframework.security.core.userdetails.User) SecurityContextHolder. getContext().getAuthentication().getPrincipal(); return userRepository.findByUsername(principal.getUsername()) .orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + principal.getUsername())); } public AuthResponse login (LoginRequest loginRequest) { Authentication authenticate = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getUsername(), loginRequest.getPassword())); SecurityContextHolder.getContext().setAuthentication(authenticate); String authToken = jwtProvider.generateToken(authenticate); return new AuthResponse(authToken, loginRequest.getUsername()); } private String encodePassword(String password) { return passwordEncoder.encode(password); } private String generateToken(User user) { String token = UUID.randomUUID().toString(); AccountVerificationToken verificationToken = new AccountVerificationToken(); verificationToken.setToken(token); verificationToken.setUser(user); tokenRepository.save(verificationToken); return token; } public void verifyToken(String token) { OptionalverificationToken = tokenRepository.findByToken(token); verificationToken.orElseThrow(() -> new ActivationException("Invalid Activation Token")); enableAccount(verificationToken.get()); } public void enableAccount(AccountVerificationToken token) { String username = token.getUser().getUsername(); User user = userRepository.findByUsername(username) .orElseThrow(() -> new ActivationException("User not found with username: " + username)); user.setAccountStatus(true); userRepository.save(user); } public boolean isLoggedIn() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return !(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated(); } }
Часть 4: Почтовая служба 🌎
Давайте рассмотрим сервис subreddit, который будет иметь наше приложение. Внутри com.your-name.backend.services добавьте следующий класс.
- PostService: Содержит логику для сопоставления данных с DTO и из DTO, получения всех сообщений, получения определенных сообщений и добавления сообщений.
package com.maxicb.backend.service; import com.github.marlonlom.utilities.timeago.TimeAgo; import com.maxicb.backend.dto.PostRequest; import com.maxicb.backend.dto.PostResponse; import com.maxicb.backend.exception.PostNotFoundException; import com.maxicb.backend.exception.SubredditNotFoundException; import com.maxicb.backend.exception.UserNotFoundException; import com.maxicb.backend.model.*; import com.maxicb.backend.repository.*; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.Instant; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @Service @AllArgsConstructor @Transactional public class PostService { private final PostRepository postRepository; private final SubredditRepository subredditRepository; private final UserRepository userRepository; private final CommentRepository commentRepository; private final AuthService authService; private final VoteRepository voteRepository; private boolean checkVoteType(Post post, VoteType voteType) { if(authService.isLoggedIn()) { OptionalvoteForPostForUser = voteRepository.findTopByPostAndUserOrderByVoteIdDesc(post, authService.getCurrentUser()); return voteForPostForUser.filter(vote -> vote.getVoteType().equals(voteType)).isPresent(); } return false; } private PostResponse mapToResponse(Post post) { return PostResponse.builder() .postId(post.getPostId()) .postTitle(post.getPostTitle()) .url(post.getUrl()) .description(post.getDescription()) .userName(post.getUser().getUsername()) .subredditName(post.getSubreddit().getName()) .voteCount(post.getVoteCount()) .commentCount(commentRepository.findByPost(post).size()) .duration(TimeAgo.using(post.getCreationDate().toEpochMilli())) .upVote(checkVoteType(post, VoteType.UPVOTE)) .downVote(checkVoteType(post, VoteType.DOWNVOTE)) .build(); } private Post mapToPost(PostRequest postRequest) { Subreddit subreddit = subredditRepository.findByName(postRequest.getSubredditName()) .orElseThrow(() -> new SubredditNotFoundException(postRequest.getSubredditName())); Post newPost = Post.builder() .postTitle(postRequest.getPostTitle()) .url(postRequest.getUrl()) .description(postRequest.getDescription()) .voteCount(0) .user(authService.getCurrentUser()) .creationDate(Instant.now()) .subreddit(subreddit) .build(); subreddit.getPosts().add(newPost); return newPost; } public PostResponse save(PostRequest postRequest) { return mapToResponse(postRepository.save(mapToPost(postRequest))); } public List getAllPost() { return StreamSupport .stream(postRepository.findAll().spliterator(), false) .map(this::mapToResponse) .collect(Collectors.toList()); } public PostResponse findByID (Long id) { Post post = postRepository.findById(id) .orElseThrow(() -> new PostNotFoundException("Post not found with id: " + id)); return mapToResponse(post); } public List getPostsBySubreddit(Long id) { Subreddit subreddit = subredditRepository.findById(id) .orElseThrow(() -> new SubredditNotFoundException("Subreddit not found with id: " + id)); return subreddit.getPosts().stream() .map(this::mapToResponse) .collect(Collectors.toList()); } public List getPostsByUsername(String username) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UserNotFoundException("User not found with username: " + username)); return postRepository.findByUser(user).stream() .map(this::mapToResponse) .collect(Collectors.toList()); } }
Часть 5: ЧТЕНИЕ И СОЗДАНИЕ конечных точек публикации 🌐
Давайте рассмотрим почтовый контроллер, который будет иметь наше приложение. Внутри com.your-name.backend.controller добавьте следующий класс.
- PostController: Удерживайте логику для выборки создаваемых сообщений, выборки всех сообщений и конкретных сообщений на основе пользователя и subreddit.
package com.maxicb.backend.controller; import com.maxicb.backend.dto.PostRequest; import com.maxicb.backend.dto.PostResponse; import com.maxicb.backend.service.PostService; import lombok.AllArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/posts") @AllArgsConstructor public class PostController { private final PostService postService; @PostMapping public ResponseEntityaddPost(@RequestBody PostRequest postRequest) { postService.save(postRequest); return new ResponseEntity<>(HttpStatus.CREATED); } @GetMapping public ResponseEntity > getAllPost() { return new ResponseEntity<>(postService.getAllPost(), HttpStatus.OK); } @GetMapping("{id}") public ResponseEntity
getPostByID(@PathVariable Long id) { return new ResponseEntity<>(postService.findByID(id), HttpStatus.OK); } @GetMapping("/sub/{id}") public ResponseEntity > getPostsBySubreddit(@PathVariable Long id) { return new ResponseEntity<>(postService.getPostsBySubreddit(id), HttpStatus.OK); } @GetMapping("/user/{name}") public ResponseEntity
> getPostsByUsername(@PathVariable String username) { return new ResponseEntity<>(postService.getPostsByUsername(username), HttpStatus.OK); } }
Часть 5: Обновленное приложение.свойства ⚙
Чтобы облегчить создание нового пользователя, прохождение регистрации и создание subreddit для тестирования недавно добавленной логики, мы собираемся обновить application.properties для сохранения наших данных. Внутри main.resources обновите свой файл application.properties, чтобы он соответствовал приведенному ниже.
# Database Properties spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.url=jdbc:postgresql://localhost:5432/postgres spring.datasource.username=postgres spring.datasource.password=admin spring.datasource.initialization-mode=always # Changing this from create-drop to update # Allows us to persist the database rather than # Dropping it each time the application is ran spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.generate-ddl=true # Redis Properties spring.cache.type=redis spring.redis.host=localhost spring.redis.port=6379 # Mail Properties spring.mail.host=smtp.mailtrap.io spring.mail.port=25 spring.mail.username=a08f0bfd316af9 spring.mail.password=ce1b93c770fc96 spring.mail.protocol=smtp
Вывод 🔍
- Чтобы убедиться, что все настроено правильно, вы можете запустить приложение и убедиться, что в консоли нет ошибок. В нижней части консоли вы должны увидеть вывод, аналогичный приведенному ниже
- Если в консоли нет ошибок, вы можете протестировать свою логику создания записи, отправив запрос post по адресу http://localhost:8080/api/posts со следующими данными. Вам все равно придется выполнить те же действия, что и в предыдущих частях, чтобы войти в учетную запись для создания сообщений, а также создать субреддит и указать действительное имя.
{ "postTitle": "Testing Post", "url": "HEREEEE", "description": "HEREEEE", "subredditName": "/r/NAME" }
- В этой статье мы добавили конечные точки СОЗДАНИЯ и ЧТЕНИЯ для post, обновили свойства нашего приложения и добавили новые исключения.
Следующий
Подписывайтесь, чтобы получить информацию о выходе седьмой части, где мы расскажем об операциях создания/чтения для комментариев! Если у вас есть какие-либо вопросы, обязательно оставьте комментарий!
Оригинал: “https://dev.to/maxicb/full-stack-reddit-clone-spring-boot-react-electron-app-part-6-5e33”