Full Stack Reddit Clone - Spring Boot, React, Electron App - Part 5
Tags: react, java, typescript, webdev
Published: 2020-08-03 16:27:09

# Full Stack Reddit Clone - Spring Boot, React, Electron App - Part 5

## Introduction

Welcome to Part 5 of creating a Reddit clone using Spring Boot, and React.

What are we building in this part?

- JWT Validation
- JWT Filtering
- Update Auth Service
- Subreddit DTO
- Subreddit Service
- READ Subreddit Endpoint
- CREATE Subreddit Endpoint

In [Part 4](https://dev.to/maxicb/full-stack-reddit-clone-spring-boot-react-electron-app-part-4-4eam) we created the logic needed for JWT validation, updated our authentication service, and made our login endpoint's!

## Important Links

- Backend Source: https://github.com/MaxiCB/vox-nobis/tree/master/backend
- Frontend Source: https://github.com/MaxiCB/vox-nobis/tree/master/client
- Live URL: In Progress

## Part 1: JWT Validation 🔐

Let's cover our JWT validation logic we will need. Inside **com.your-name.backend.security** we will update the following classes.

- JWTProvider: Handles all of the logic for loading the keystore, gathering public/private keys, generating JWT, token validation, and gathering user information.

```
package com.maxicb.backend.security;

import com.maxicb.backend.exception.ActivationException;
import io.jsonwebtoken.Claims;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.InputStream;
import java.security.*;
import java.security.cert.CertificateException;

import io.jsonwebtoken.Jwts;
import static io.jsonwebtoken.Jwts.parserBuilder;

@Service
public class JWTProvider {
    private KeyStore keystore;

    @PostConstruct
    public void init() {
        try {
            keystore = KeyStore.getInstance("JKS");
            InputStream resourceStream = getClass().getResourceAsStream("/reddit.jks");
            keystore.load(resourceStream, "secret".toCharArray());
        } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
            throw new ActivationException("Exception occured while loading keystore");
        }
    }

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

    public boolean validateToken (String token) {
        parserBuilder().setSigningKey(getPubKey()).build().parseClaimsJws(token);
        return true;
    }

    private PrivateKey getPrivKey () {
        try {
            return (PrivateKey) keystore.getKey("vox", "secret".toCharArray());
        } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
            throw new ActivationException("Exception occurred while retrieving public key");
        }
    }

    private PublicKey getPubKey () {
        try {
            return keystore.getCertificate("vox").getPublicKey();
        } catch(KeyStoreException e) {
            throw new ActivationException("Exception occurred retrieving public key");
        }
    }

    public String getNameFromJWT(String token) {
        Claims claims = parserBuilder()
                .setSigningKey(getPubKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
        return claims.getSubject();
    }
}
```

## Part 2: JWT Filtering 🗄

Let's cover our JWT filtering logic we will need. Inside **com.your-name.backend.security** we will update the following class.

- JWTAuthFilter: Handles loading JWT from request, and filtering.

```
package com.maxicb.backend.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JWTAuthFilter extends OncePerRequestFilter {
    @Autowired
    private JWTProvider jwtProvider;
    @Qualifier("inMemoryUserDetailsManager")
    @Autowired
    private UserDetailsService userDetailsService;

    private String getTokenFromRequest (HttpServletRequest request) {
        String bearer = request.getHeader("Authorization");
        if(StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) {
            return bearer.substring(7);
        }
        return bearer;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        String JWT = getTokenFromRequest(httpServletRequest);

        if (StringUtils.hasText(JWT) && jwtProvider.validateToken(JWT)) {
            String username = jwtProvider.getNameFromJWT(JWT);

            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
                    userDetails,
                    null,
                    userDetails.getAuthorities());
            auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

```

## Part 3: Updated Security Configuration 💂‍♀️

Let's cover our JWT validation logic we will need. Inside **com.your-name.backend.config** we will update the following class.

- Security: We need to implement our newly created JWT Filter so that it will run first.

```
package com.maxicb.backend.config;

import com.maxicb.backend.security.JWTAuthFilter;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@EnableWebSecurity
@AllArgsConstructor
public class Security extends WebSecurityConfigurerAdapter {

    UserDetailsService userDetailsService;
    JWTAuthFilter jwtAuthFilter;

    @Autowired
    public void configureGlobalConfig(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable()
                .authorizeRequests()
                .antMatchers("/api/auth/**")
                .permitAll()
                .anyRequest()
                .authenticated();
        httpSecurity.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

```

## Part 4: Updated Auth Service 💂‍♀️

Let's cover our JWT validation logic we will need. Inside **com.your-name.backend.service** we will update the following class.

- AuthService: We need to implement logic to get the current user information.

```
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.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<AccountVerificationToken> 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);
    }
}

```

## Part 5: Subreddit DTO 📨

Let's cover the Subreddit DTO class we will need. Inside **com.your-name.backend.dto** create the following class,

- SubredditDTO: Defines the data that our backend will send/receive from the client during a subreddit requests.

```
package com.maxicb.backend.dto;

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

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SubredditDTO {
    private Long id;
    private String name;
    private String description;
    private Integer postCount;
    private Integer memberCount;
}

```

## Part 6: Subreddit Exception 🚫

Let's cover the subreddit not found exception our application will have. Inside **com.your-name.backend.exception** add the following class.

- SubredditNotFoundException: Our custom exception handler for subreddit not found errors.

```
package com.maxicb.backend.exception;

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

## Part 6: Subreddit Service 🌎

Let's cover the subreddit service our application will have. Inside **com.your-name.backend.services** add the following class.

- SubredditService: Hold the logic for mapping data to and from DTO, getting all subreddits, getting specific subreddits, and adding subreddits.

```
package com.maxicb.backend.service;

import com.maxicb.backend.dto.SubredditDTO;
import com.maxicb.backend.exception.SubredditNotFoundException;
import com.maxicb.backend.model.Subreddit;
import com.maxicb.backend.repository.SubredditRepository;
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.stream.Collectors;
import java.util.stream.StreamSupport;

@Service
@AllArgsConstructor
public class SubredditService {

    private final SubredditRepository subredditRepository;
    private final AuthService authService;

    private SubredditDTO mapToDTO (Subreddit subreddit) {
        return SubredditDTO.builder()
                .id(subreddit.getId())
                .name(subreddit.getName())
                .description(subreddit.getDescription())
                .postCount(subreddit.getPosts().size())
                .build();

    }

    private Subreddit mapToSubreddit (SubredditDTO subredditDTO) {
        return Subreddit.builder().name("/r/" + subredditDTO.getName())
                .description(subredditDTO.getDescription())
                .user(authService.getCurrentUser())
                .creationDate(Instant.now())
                .build();

    }

    @Transactional(readOnly = true)
    public List<SubredditDTO> getAll() {
        return StreamSupport
                .stream(subredditRepository.findAll().spliterator(), false)
                .map(this::mapToDTO)
                .collect(Collectors.toList());
    }

    @Transactional
    public SubredditDTO save(SubredditDTO subredditDTO) {
        Subreddit subreddit = subredditRepository.save(mapToSubreddit(subredditDTO));
        subredditDTO.setId(subreddit.getId());
        return subredditDTO;
    }

    @Transactional(readOnly = true)
    public SubredditDTO getSubreddit(Long id) {
        Subreddit subreddit = subredditRepository.findById(id)
                .orElseThrow(() -> new SubredditNotFoundException("Subreddit not found with id -" + id));
        return mapToDTO(subreddit);
    }
}

```

## Part 7: READ && CREATE Subreddit Endpoint's 🌐

Let's cover the subreddit controller our application will have. Inside **com.your-name.backend.controller** add the following class.

- SubredditController: Hold the logic for fetching creating subreddits, fetching all subreddits, and specific subreddits.

```
package com.maxicb.backend.controller;

import com.maxicb.backend.dto.SubredditDTO;
import com.maxicb.backend.service.SubredditService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.List;

@RestController
@RequestMapping("/api/subreddit")
@AllArgsConstructor
public class SubredditController {

    SubredditService subredditService;

    @GetMapping
    public List<SubredditDTO> getAllSubreddits () {
        return subredditService.getAll();
    }

    @GetMapping("/{id}")
    public SubredditDTO getSubreddit(@PathVariable Long id) {
        return subredditService.getSubreddit(id);
    }

    @PostMapping
    public SubredditDTO addSubreddit(@RequestBody @Valid SubredditDTO subredditDTO) {
        return subredditService.save(subredditDTO);
    }
}

```

## Conclusion 🔍

- To ensure everything is configured correctly you can run the application, and ensure there are no error in the console. Towards the bottom of the console you should see output similar to below

  ![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/mcnzflm8vjwnvjcx40sh.PNG "Successful Run")

- If there are no error's in the console you can test you registration logic by sending a post request to **http://localhost:8080/api/auth/register** with the following data

```
{
    "username": "test",
    "email": "test1@test.com",
    "password": "test12345"
}
```

- Once you recieve a 200 OK status back you can check you mailtrap.io inbox to find the activation email that was sent. The link should look similar to http://localhost:8080/api/auth/verify/{token}, **be sure to omit the &lt** from the end of the link. Navigation to the link will activate the account, and you should see "Account Activated" displayed as a response.

- After activating your account you can test you login logic by sending a post request to **http://localhost:8080/api/auth/login** with the following data

```
{
    "username": "test",
    "password": "test12345"
}
```

- After logging in you should see a response similar to below

```
{
    "authenticationToken": {real_long_token},
    "username": "test"
}
```

- Add the token to the API testing tool you use as the authorization header, set as the Bearer Token type.

- You can now send a POST request to **http://localhost:8080/api/subreddit/** with the following data

```
{
    "name": "NAME",
    "description": "DESCRIPTION",
    "postCount": null
}
```

- You should recieve a 200 OK with the subreddit information back as a response, to ensure it was saved in the database you can send a GET request to **http://localhost:8080/api/subreddit** and you should recieve the following data

```
[
    {
        "id": 1,
        "name": "NAME",
        "description": "DESCRIPTION",
        "postCount": null
    }
]
```

- In this article we added our JWT validation, and filtering, updated our authentication services, and implemented the CREATE && READ endpoints for subreddit's.

## Next

### [Part 6](https://dev.to/maxicb/full-stack-reddit-clone-spring-boot-react-electron-app-part-6-5e33) is released, where we will cover the Create/Read operations for posts! If you have any questions be sure to leave a comment!