Рубрики
Без рубрики

Полный стек Reddit Clone – Spring Boot, React, Electron App – Часть 6

Полный стек Reddit Clone – Spring Boot, React, Electron App – Введение в часть 6… С тегами react, java, web dev, typescript.

Вступление

Добро пожаловать в Часть 6 создания клона Reddit с использованием Spring Boot и React.

Что мы строим в этой части?

  • Отправить запрос НА
  • Опубликовать ответ НА
  • Пользовательские исключения
  • Обновленная Служба Авторизации
  • Почтовая служба
  • ЧТЕНИЕ конечных точек Post
  • Создать конечную точку Post
  • Обновленное приложение.свойства

В части 5 мы создали логику, необходимую для фильтрации JWT, обновили нашу службу аутентификации и сделали нашу конечную точку subreddit!

Важные ссылки

Часть 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) {
        Optional verificationToken = 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()) {
            Optional voteForPostForUser = 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 ResponseEntity addPost(@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”