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

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

Полный стек Reddit Clone – Spring Boot, React, Electron App – Часть 9 … С тегами react, java, typescript, javascript.

Вступление

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

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

  • Поддержка разбивки на страницы
    • Мы обновим наш серверный сервер для поддержки разбивки на страницы, это сократит время загрузки клиента по мере того, как база данных начнет масштабироваться
  • Признание JWT недействительным
  • JWT Освежающий

В части 8 мы добавили конечные точки СОЗДАНИЯ и ЧТЕНИЯ для создания и чтения комментариев!!

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

Часть 1: Обновление репозиториев 🗄

Давайте рассмотрим обновление всех наших репозиториев для реализации поддержки разбивки на страницы и сортировки. Внутри com.your-name.backend.repository мы обновим следующие классы.

  • CommentRespository: Мы преобразуем вашу существующую логику, а также добавим метод findAllByPost, который по-прежнему возвращает список, поскольку мы полагаемся на него для отправки обратно общего количества комментариев в нашем PostService
package com.maxicb.backend.repository;

import com.maxicb.backend.model.Comment;
import com.maxicb.backend.model.Post;
import com.maxicb.backend.model.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.PagingAndSortingRepository;

import java.util.List;

public interface CommentRepository extends PagingAndSortingRepository {
    Page findByPost(Post post, Pageable pageable);
    List findAllByPost(Post post);
    Page findAllByUser(User user, Pageable pageable);
}
  • Пострепозиционный:
package com.maxicb.backend.repository;

import com.maxicb.backend.model.Post;
import com.maxicb.backend.model.Subreddit;
import com.maxicb.backend.model.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;

import java.util.List;

public interface PostRepository extends PagingAndSortingRepository {
    Page findAllBySubreddit(Subreddit subreddit, Pageable pageable);
    Page findByUser(User user, Pageable pageable);
}
  • SubredditRepository:
package com.maxicb.backend.repository;

import com.maxicb.backend.model.Subreddit;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.PagingAndSortingRepository;

import java.util.Optional;

public interface SubredditRepository extends PagingAndSortingRepository {
    Optional findByName(String subredditName);
    Optional> findByNameLike(String subredditName, Pageable pageable);
}

Часть 2: Обновление служб 🌎

Теперь, когда мы обновили наши репозитории, нам нужно будет обновить наши серверы, чтобы отразить эти изменения. Внутри com.your-name.backend.service мы обновим следующие классы. Имейте в виду, что я не буду отображать весь класс в разделе, а только конкретные методы, которые мы будем обновлять.

  • CommentService: Мы обновим getCommentsForPost && get Comments Для пользовательских методов, чтобы правильно обрабатывать разбивку на страницы
    public Page getCommentsForPost(Long id, Integer page) {
        Post post = postRepository.findById(id)
                .orElseThrow(() -> new PostNotFoundException("Post not found with id: " + id));
        return commentRepository.findByPost(post, PageRequest.of(page, 100)).map(this::mapToResponse);
    }

    public Page getCommentsForUser(Long id, Integer page) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
        return commentRepository.findAllByUser(user, PageRequest.of(page, 100)).map(this::mapToResponse);
    }
  • Почтовое обслуживание: Мы обновим карту До методов Response && get All Posts && getPostsBySubreddit && getPostsByUsername для реализации разбивки на страницы, а также сохраним существующую логику сопоставления с DTO
    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.findAllByPost(post).size())
                .duration(TimeAgo.using(post.getCreationDate().toEpochMilli()))
                .upVote(checkVoteType(post, VoteType.UPVOTE))
                .downVote(checkVoteType(post, VoteType.DOWNVOTE))
                .build();
    }

    public Page getAllPost(Integer page) {
        return postRepository.findAll(PageRequest.of(page, 100)).map(this::mapToResponse);
    }

    public Page getPostsBySubreddit(Integer page, Long id) {
        Subreddit subreddit = subredditRepository.findById(id)
                .orElseThrow(() -> new SubredditNotFoundException("Subreddit not found with id: " + id));
        return postRepository
                .findAllBySubreddit(subreddit, PageRequest.of(page, 100))
                .map(this::mapToResponse);
    }

    public Page getPostsByUsername(String username, Integer page) {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UserNotFoundException("User not found with username: " + username));
        return postRepository
                .findByUser(user, PageRequest.of(page, 100))
                .map(this::mapToResponse);
    }
  • SubredditService: Мы обновим метод GetAll
    @Transactional(readOnly = true)
    public Page getAll(Integer page) {
        return subredditRepository.findAll(PageRequest.of(page, 100))
                .map(this::mapToDTO);
    }

Часть 3: Обновление контроллеров

Теперь, когда мы обновили наши сервисы и репозитории, нам нужно будет обновить наши контроллеры, чтобы позволить клиенту использовать разбивку на страницы. Внутри com.your-name.backend.controller мы обновим следующие классы. Имейте в виду, что я не буду отображать весь класс в разделе, а только конкретные методы, которые мы будем обновлять.

  • CommentController: Мы обновим методы get Comments By Post && getCommentsByUser для правильной обработки разбивки на страницы
    @GetMapping("/post/{id}")
    public ResponseEntity> getCommentsByPost(@PathVariable("id") Long id, @RequestParam Optional page) {
        return new ResponseEntity<>(commentService.getCommentsForPost(id, page.orElse(0)), HttpStatus.OK);
    }

    @GetMapping("/user/{id}")
    public ResponseEntity> getCommentsByUser(@PathVariable("id") Long id,@RequestParam Optional page) {
        return new ResponseEntity<>(commentService.getCommentsForUser(id, page.orElse(0)), HttpStatus.OK);
    }
  • PostController: Сначала мы обновим метод add Post, чтобы отправить созданную запись обратно клиенту при успешном создании, методы getAllPost && getPostsBySubreddit && getPostsByUsername для реализации разбивки на страницы
    @PostMapping
    public ResponseEntity addPost(@RequestBody PostRequest postRequest) {
        return new ResponseEntity<>(postService.save(postRequest), HttpStatus.CREATED);
    }

    @GetMapping
    public ResponseEntity> getAllPost(@RequestParam Optional page) {
        return new ResponseEntity<>(postService.getAllPost(page.orElse(0)), HttpStatus.OK);
    }

    @GetMapping("/sub/{id}")
    public ResponseEntity> getPostsBySubreddit(@PathVariable Long id, @RequestParam Optional page) {
        return new ResponseEntity<>(postService.getPostsBySubreddit(page.orElse(0), id), HttpStatus.OK);
    }

    @GetMapping("/user/{name}")
    public ResponseEntity> getPostsByUsername(@PathVariable("name") String username, @RequestParam Optional page) {
        return new ResponseEntity<>(postService.getPostsByUsername(username, page.orElse(0)), HttpStatus.OK);
    }
  • SubredditController: Мы обновим все методы для реализации отправки ResponseEntity, а также поддержки разбивки на страницы
    @GetMapping("/{page}")
    public ResponseEntity> getAllSubreddits (@PathVariable("page") Integer page) {
        return new ResponseEntity<>(subredditService.getAll(page), HttpStatus.OK);
    }

    @GetMapping("/sub/{id}")
    public ResponseEntity getSubreddit(@PathVariable("id") Long id) {
        return new ResponseEntity<>(subredditService.getSubreddit(id), HttpStatus.OK);
    }

    @PostMapping
    public ResponseEntity addSubreddit(@RequestBody @Valid SubredditDTO subredditDTO) throws Exception{
        try {
            return new ResponseEntity<>(subredditService.save(subredditDTO), HttpStatus.OK);
        } catch (Exception e) {
            throw new Exception("Error Creating Subreddit");
        }
    }

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

Часть 5: Обновить класс токена ⏳

Теперь нам нужно создать ваш класс токенов обновления, этот класс будет иметь идентификатор, токен и связанную с ним дату создания, чтобы разрешить аннулирование токенов по истечении заданного промежутка времени.

  • refreshToken:
package com.maxicb.backend.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.time.Instant;

@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor
public class RefreshToken {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String token;
    private Instant creationDate;
}

Часть 5: Обновление службы токенов и DTO 🌎

Теперь, когда у нас есть наш refreshToken, мы подготовим все необходимое, чтобы начать обновление нашей системы аутентификации. Внутри проекта мы добавим и обновим следующие классы.

  • RefreshTokenRepository:
package com.maxicb.backend.repository;

import com.maxicb.backend.model.RefreshToken;
import org.springframework.data.repository.PagingAndSortingRepository;

import java.util.Optional;

public interface RefreshTokenRepository extends PagingAndSortingRepository {
    Optional findByToken(String token);

    void deleteByToken(String token);
}
  • RefreshTokenService: Эта служба позволит нам генерировать токены, проверять токены и удалять токены.
package com.maxicb.backend.service;

import com.maxicb.backend.exception.VoxNobisException;
import com.maxicb.backend.model.RefreshToken;
import com.maxicb.backend.repository.RefreshTokenRepository;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.util.UUID;

@Service
@AllArgsConstructor
@Transactional
public class RefreshTokenService {
    private RefreshTokenRepository refreshTokenRepository;

    RefreshToken generateRefreshToken () {
        RefreshToken refreshToken = new RefreshToken();
        refreshToken.setToken(UUID.randomUUID().toString());
        refreshToken.setCreationDate(Instant.now());
        return refreshTokenRepository.save(refreshToken);
    }

    void validateToken(String token) {
        refreshTokenRepository.findByToken(token)
                .orElseThrow(() -> new VoxNobisException("Invalid Refresh Token"));
    }

    public void deleteRefreshToken(String token) {
        refreshTokenRepository.deleteByToken(token);
    }
}
  • Обновленный authResponse: Мы обновим authResponse, чтобы включить наш недавно сгенерированный токен.
import lombok.AllArgsConstructor;
import lombok.Data;

import java.time.Instant;

@Data
@AllArgsConstructor
public class AuthResponse {
        private String authenticationToken;
        private String refreshToken;
        private Instant expiresAt;
        private String username;
}
  • RefreshTokenRequest: Этот DTO будет обрабатывать запросы от клиента на обновление их токена до истечения срока его действия в системе
package com.maxicb.backend.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotBlank;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class RefreshTokenRequest {
    @NotBlank
    private String refreshToken;
    private String username;
}

Часть 6: Обновление поставщика JWT 🔏

Теперь, когда у нас все готово, мы начнем обновлять нашу систему JWT. Внутри com.your-name.backend.service мы обновим следующие классы. Имейте в виду, что я не буду отображать весь класс в разделе, а только конкретные методы, которые мы будем обновлять.

  • JWTProvider: Мы обновим нашу СОБСТВЕННУЮ реализацию, включив дату выпуска, а также установим дату истечения срока действия при создании нового токена.
@Service
public class JWTProvider {
    private KeyStore keystore;
    @Value("${jwt.expiration.time}")
    private Long jwtExpirationMillis;

    ...
    ....
    public String generateToken(Authentication authentication) {
        org.springframework.security.core.userdetails.User princ = (User) authentication.getPrincipal();
        return Jwts.builder()
                .setSubject(princ.getUsername())
                .setIssuedAt(from(Instant.now()))
                .signWith(getPrivKey())
                .setExpiration(from(Instant.now().plusMillis(jwtExpirationMillis)))
                .compact();
    }

    public String generateTokenWithUsername(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(from(Instant.now()))
                .signWith(getPrivKey())
                .setExpiration(from(Instant.now().plusMillis(jwtExpirationMillis)))
                .compact();
    }
    ....
    ...
    public Long getJwtExpirationMillis() {
        return jwtExpirationMillis;
    }

Часть 7: Обновленная аутентификация 💂 ♀ ️

Теперь, когда мы внедрили разбивку на страницы, мы начнем обновлять нашу систему аутентификации. Внутри нашего проекта мы обновим следующие классы. Имейте в виду, что я не буду отображать весь класс в разделе, а только конкретные методы, которые мы будем обновлять.

  • AuthService: Мы обновим наш AuthService для обработки отправки токенов обновления и добавим логику для обновления существующих токенов.
public AuthResponse refreshToken(RefreshTokenRequest refreshTokenRequest) {
        refreshTokenService.validateToken(refreshTokenRequest.getRefreshToken());
        String token = jwtProvider.generateTokenWithUsername(refreshTokenRequest.getUsername());
        return new AuthResponse(token, refreshTokenService.generateRefreshToken().getToken(), Instant.now().plusMillis(jwtProvider.getJwtExpirationMillis()), refreshTokenRequest.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);
        String refreshToken = refreshTokenService.generateRefreshToken().getToken();
        return new AuthResponse(authToken, refreshToken, Instant.now().plusMillis(jwtProvider.getJwtExpirationMillis()), loginRequest.getUsername());
    }
  • AuthController: Теперь мы внедрим новые конечные точки, чтобы позволить клиенту использовать недавно добавленную логику.
@PostMapping("/refresh/token")
    public AuthResponse refreshToken(@Valid @RequestBody RefreshTokenRequest refreshTokenRequest) {
        return authService.refreshToken(refreshTokenRequest);
    }

    @PostMapping("/logout")
    public ResponseEntity logout(@Valid @RequestBody RefreshTokenRequest refreshTokenRequest) {
        refreshTokenService.deleteRefreshToken(refreshTokenRequest.getRefreshToken());
        return ResponseEntity.status(HttpStatus.OK).body("Refresh Token Deleted");
    }

Часть 8: Пользовательское исключение 🚫

  • VoxNobisException: Мы создадим настраиваемое исключение общего назначения, которое можно использовать повторно во всем нашем приложении по мере его расширения.
package com.maxicb.backend.exception;

public class VoxNobisException extends RuntimeException {
    public VoxNobisException(String message) {super(message);}
}

Часть 9: Обновлено приложение.свойства

Нам нужно будет добавить время истечения срока действия, которое мы хотели бы, чтобы наше приложение использовало, когда дело доходит до генерации токенов, и соответствующим образом установить даты их истечения. Я решил установить его на 15 минут, но в будущем увеличу продолжительность.

# JWT Properties
jwt.expiration.time=900000

Часть 10: Реализация пользовательского интерфейса Swagger 📃

Теперь, когда мы подошли к концу нашего бэкэнда MVP, мы добавим Swagger UI. Если вы никогда раньше не использовали Swagger, это отличный способ автоматически создавать документацию для вашего API. Вы можете узнать больше здесь!

  • pom.xml: Нам нужно будет включить зависимости swagger в ваш проект pom.xml файл.
        
            io.springfox
            springfox-swagger2
            2.9.2
        
        
            io.springfox
            springfox-swagger-ui
            2.9.2
        
  • SwaggerConfig: Внутри com.your-name.backend.config мы создадим следующий класс.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket voxNobisAPI() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.any())
                .build()
                .apiInfo(getAPIInfo());
    }

    private ApiInfo getAPIInfo(){
        return new ApiInfoBuilder()
                .title("Vox-Nobis API")
                .version("1.0")
                .description("API for Vox-Nobis reddit clone")
                .build();
    }
}
  • Серверное приложение: Внутри com.your-name.backend мы введем нашу конфигурацию Swagger.
@SpringBootApplication
@EnableAsync
@Import(SwaggerConfig.class)
public class BackendApplication {
    ...
}
  • Безопасность: Если вы запустите приложение сейчас и попытаетесь перейти к http://localhost:8080/swagger-ui.html#/ , вы , скорее всего, получите ошибку 403 forbidden. Внутри com.your-name.backend.config нам нужно будет обновить нашу конфигурацию безопасности, чтобы разрешить доступ без авторизации, добавив следующие сопоставления под нашим существующим.
.antMatchers(HttpMethod.GET, "/api/subreddit")
.permitAll()
.antMatchers("/v2/api-docs",
            "/configuration/ui",
            "/swagger-resources/**",
            "/configuration/security",
            "/swagger-ui.html",
            "/webjars/**")
.permitAll()

Вывод 🔍

  • Чтобы убедиться, что все настроено правильно, вы можете запустить приложение и убедиться, что в консоли нет ошибок. В нижней части консоли вы должны увидеть вывод, аналогичный приведенному ниже
  • Если в консоли нет ошибок, вы можете протестировать новую логику, отправив post-запрос на http://localhost:8080/api/auth/login с правильными данными, после успешного входа в систему вы должны получить refreshToken и имя пользователя обратно прямо сейчас!

  • Вы также можете перейти к http://localhost:8080/swagger-ui.html #/ , и просмотрите документацию для всех созданных нами конечных точек, а также необходимую им информацию и вернитесь.

  • В этой статье мы добавили разбивку на страницы и время истечения срока действия токена.

Следующий

Следите за новостями, чтобы получить информацию о выходе десятой части, где мы начнем работать над интерфейсом приложения!

Оригинал: “https://dev.to/maxicb/full-stack-reddit-clone-spring-boot-react-electron-app-part-9-3pj5”