Full Stack Polling Application with Spring Boot & Spring Security & JWT & MySQL & React

Hello and Welcome to the first part of an exciting series of blog posts where you will learn how to build an end-to-end full stack polling…

Full Stack Polling Application with Spring Boot & Spring Security & JWT & MySQL & React

Hello and Welcome to the first part of an exciting series of blog posts where you will learn how to build an end-to-end full stack polling app similar to twitter polls.

We’ll build the backend server using Spring Boot where we’ll use Spring Security along with JWT authentication. We’ll use MySQL database for storage.

The front-end application will be built using React. We’ll also use Ant Design for designing our user interface.

In the end of this tutorial series, you’ll have built a fully-fledged polling application from scratch like a boss.

The complete source code of the project is hosted on Github. You can refer that anytime if you get stuck at something.

Following is the screenshot of the final version of our application -

Looks great, isn’t it? Well, then let’s start building it from scratch…

In this article, We’ll set up the backend project using Spring Boot and define the basic domain models and repositories.

Creating the Backend Application using Spring Boot

Let’s bootstrap the project using Spring Initialzr web tool -

  1. Open http://start.spring.io
  2. Enter polls in Artifact field.
  3. Add Web, JPA, MySQL and Security dependencies from the Dependencies section.
  4. Click Generate to generate and download the project.

Once the project is downloaded, unzip it and import it into your favorite IDE. The directory structure of the project will look like this-

Adding additional dependencies

We’ll need to add few additional dependencies to our project. Open pom.xml file from the root directory of your generated project and add the following to the <dependencies> section -

<!-- For Working with Json Web Tokens (JWT) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>

<!-- For Java 8 Date/Time Support -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

Configuring the Server, Database, Hibernate and Jackson

Let’s now configure the server, database, hibernate, and jackson by adding the following properties to the src/main/resources/application.properties file -

## Server Properties
server.port= 8080
server.compression.enabled=true

## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.url= jdbc:mysql://localhost:3306/polling_app?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false
spring.datasource.username= root
spring.datasource.password= 12345678


## Hibernate Properties
# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto = update

## Hibernate Logging
logging.level.org.hibernate.SQL= DEBUG

# Initialize the datasource with available DDL and DML scripts
spring.datasource.initialization-mode=always

## Jackson Properties
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS= false
spring.jackson.time-zone= UTC

All the above properties are self-explanatory. I’ve set hibernate’s ddl-auto property to update. This will automatically create/update the tables in the database according to the entities in our application.

The Jackson’s WRITE_DATES_AS_TIMESTAMPS property is used to disable serializing Java 8 Data/Time values as timestamps. All the Date/Time values will be serialized to ISO date/time string.

Before proceeding further, please create a database named polling_app in MySQL and change the spring.datasource.username and spring.datasource.password properties as per your MySQL installation.

Configuring Spring Boot to use Java 8 Date/Time converters and UTC Timezone

We’ll be using Java 8 Data/Time classes in our domain models. We’ll need to register JPA 2.1 converters so that all the Java 8 Date/Time fields in the domain models automatically get converted to SQL types when we persist them in the database.

Moreover, We’ll set the default timezone for our application to UTC.

Open the main class PollsApplication.java and make the following modifications to it-

package com.example.polls;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters;

import javax.annotation.PostConstruct;
import java.util.TimeZone;

@SpringBootApplication
@EntityScan(basePackageClasses = {
PollsApplication.class,
Jsr310JpaConverters.class
})
public class PollsApplication {

@PostConstruct
void init() {
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
}

public static void main(String[] args) {
SpringApplication.run(PollsApplication.class, args);
}
}

Creating the domain models

Our application will allow new users to register and login to our application. Every User will have one or more roles. The roles associated with a user will be used in future to decide whether the user is authorized to access a particular resource on our server or not.

In this section, We’ll create the User and Role domain models. All the domain models will be stored in a package named model inside com.example.polls.

1. User model

The User model contains the following fields -

  1. id: Primary Key
  2. username: A unique username
  3. email: A unique email
  4. password: A password which will be stored in encrypted format.
  5. roles: A set of roles. (Many-To-Many relationship with Role entity)

Here is the complete User class -

package com.example.polls.model;

import com.example.polls.model.audit.DateAudit;
import org.hibernate.annotations.NaturalId;
import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.util.HashSet;
import java.util.Set;

/**
* Created by DimuthuKasun on 01/05/20.
*/

@Entity
@Table(name = "users", uniqueConstraints = {
@UniqueConstraint(columnNames = {
"username"
}),
@UniqueConstraint(columnNames = {
"email"
})
})
public class User extends DateAudit {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotBlank
@Size(max = 40)
private String name;

@NotBlank
@Size(max = 15)
private String username;

@NaturalId
@NotBlank
@Size(max = 40)
@Email
private String email;

@NotBlank
@Size(max = 100)
private String password;

@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();

public User() {

}

public User(String name, String username, String email, String password) {
this.name = name;
this.username = username;
this.email = email;
this.password = password;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public Set<Role> getRoles() {
return roles;
}

public void setRoles(Set<Role> roles) {
this.roles = roles;
}
}

The User class extends the DateAudit class that we’ll define shortly. The DateAudit class will have createdAt and updatedAt fields that will be used for auditing purposes.

2. Role model

The Role class contains an id and a name field. The name field is an enum. We’ll have a fixed set of pre-defined roles. So it makes sense to make the role name as enum.

Here is the complete code for Role class -

package com.example.polls.model;

import org.hibernate.annotations.NaturalId;
import javax.persistence.*;

/**
* Created by DimuthuKasun on 01/05/20.
*/
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Enumerated(EnumType.STRING)
@NaturalId
@Column(length = 60)
private RoleName name;

public Role() {

}

public Role(RoleName name) {
this.name = name;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public RoleName getName() {
return name;
}

public void setName(RoleName name) {
this.name = name;
}

}

I have defined two roles namely ROLE_USER and ROLE_ADMIN. You’re free to add more roles as per your project requirements.

3. DateAudit model

All right! Let’s now define the DateAudit model. It will have a createdAt and an updatedAt field. Other domain models that need these auditing fields will simply extend this class.

We’ll use JPA’s AuditingEntityListener to automatically populate createdAt and updatedAt values when we persist an entity.

Here is the Complete DateAudit class (I’ve created a package named audit inside com.example.polls.model package to store all the auditing related models) -

package com.example.polls.model.audit;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.io.Serializable;
import java.time.Instant;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@JsonIgnoreProperties(
value = {"createdAt", "updatedAt"},
allowGetters = true
)
public abstract class DateAudit implements Serializable {

@CreatedDate
private Instant createdAt;

@LastModifiedDate
private Instant updatedAt;

public Instant getCreatedAt() {
return createdAt;
}

public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}

public Instant getUpdatedAt() {
return updatedAt;
}

public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}

}

To enable JPA Auditing, we’ll need to add @EnableJpaAuditing annotation to our main class or any other configuration classes.

Let’s create an AuditingConfig configuration class and add the @EnableJpaAuditing annotation to it.

We’re creating a separate class because we’ll be adding more auditing related configurations later. So it’s better to have a separate class.

We’ll keep all the configuration classes inside a package named config. Go ahead and create the config package inside com.example.polls, and then create the AuditingConfig class inside config package -

package com.example.polls.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@Configuration
@EnableJpaAuditing
public class AuditingConfig {

// That's all here for now. We'll add more auditing configurations later.
}

Creating the Repositories for accessing User and Role data

Now that we have defined the domain models, Let’s create the repositories for persisting these domain models to the database and retrieving them.

All the repositories will go inside a package named repository. So let’s first create the repository package inside com.example.polls.

1. UserRepository

Following is the complete code for UserRepository interface. It extends Spring Data JPA’s JpaRepository interface.

package com.example.polls.repository;

import com.example.polls.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);

Optional<User> findByUsernameOrEmail(String username, String email);

List<User> findByIdIn(List<Long> userIds);

Optional<User> findByUsername(String username);

Boolean existsByUsername(String username);

Boolean existsByEmail(String email);
}

2. RoleRepository

Following is the RoleRepository interface. It contains a single method to retrieve a Role from the RoleName-

package com.example.polls.repository;
import com.example.polls.model.Role;
import com.example.polls.model.RoleName;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByName(RoleName roleName);
}

Exploring the current setup and Running the Application

After creating all the above models, repositories and configurations, our current project should look like this -

You can run the application by typing the following command from the root directory of your project -

mvn spring-boot:run

Check out the logs and make sure that the server starts successfully.

2018-02-24 22:40:44.998  INFO 33708 --- Tomcat started on port(s): 5000 (http)
2018-02-24 22:40:45.008 INFO 33708 --- Started PollsApplication in 7.804 seconds (JVM running for 27.193)

Write to me in the discussion section, if the server doesn’t start successfully for you. I’ll help you out.

Creating Default Roles

We’ll have a fixed set of predefined roles in our application. Whenever a user logs in, we’ll assign ROLE_USER to it by default.

For assigning the roles, they have to be present in the database. So let’s create the two default roles in the database by executing the following insert statements -

INSERT INTO roles(name) VALUES('ROLE_USER');
INSERT INTO roles(name) VALUES('ROLE_ADMIN');

Then, We’ll configure Spring Security along with JWT authentication, and write the APIs to let users register and login to our application.

An overview of the security mechanism that we’re going to build

  • Build an API that registers new users with their name, username, email and password.
  • Build an API to let users log in using their username/email and password. After validating user’s credentials, the API should generate a JWT authentication token and return the token in the response.
  • The clients will send this JWT token in the Authorization header of all the requests to access any protected resources.
  • Configure Spring security to restrict access to protected resources. For example,
  • APIs for login, signup, and any static resources like images, scripts and stylesheets should be accessible to everyone.
  • APIs to create a poll, vote to a poll etc, should be accessible to authenticated users only.
  • Configure Spring security to throw a 401 unauthorized error if a client tries to access a protected resource without a valid JWT token.

Configuring Spring Security and JWT

The following class is the crux of our security implementation. It contains almost all the security configurations that are required for our project.

Let’s first create the following SecurityConfig class inside the package com.example.polls.config, and then we’ll go through the code and learn what each configuration does -

package com.example.polls.config;
import com.example.polls.security.CustomUserDetailsService;
import com.example.polls.security.JwtAuthenticationEntryPoint;
import com.example.polls.security.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
securedEnabled = true,
jsr250Enabled = true,
prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CustomUserDetailsService customUserDetailsService;
    @Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
    @Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
    @Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder());
}
    @Bean(BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
    @Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
    @Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/",
"/favicon.ico",
"/**/*.png",
"/**/*.gif",
"/**/*.svg",
"/**/*.jpg",
"/**/*.html",
"/**/*.css",
"/**/*.js")
.permitAll()
.antMatchers("/api/auth/**")
.permitAll()
.antMatchers("/api/user/checkUsernameAvailability", "/api/user/checkEmailAvailability")
.permitAll()
.antMatchers(HttpMethod.GET, "/api/polls/**", "/api/users/**")
.permitAll()
.anyRequest()
.authenticated();
        // Add our custom JWT security filter
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

The above SecurityConfig class will show compilation errors in your IDE because we haven’t yet defined many of the classes that are being used in it. We will define them one by one in this article.

But before that, Let’s understand the meaning of the annotations and configurations used in the SecurityConfig class -

1. @EnableWebSecurity

This is the primary spring security annotation that is used to enable web security in a project.

2. @EnableGlobalMethodSecurity

This is used to enable method level security based on annotations. You can use following three types of annotations for securing your methods -

  • securedEnabled: It enables the @Secured annotation using which you can protect your controller/service methods like so -

@Secured("ROLE_ADMIN") public User getAllUsers() {} @Secured({"ROLE_USER", "ROLE_ADMIN"}) public User getUser(Long id) {} @Secured("IS_AUTHENTICATED_ANONYMOUSLY") public boolean isUsernameAvailable() {}

  • jsr250Enabled: It enables the @RolesAllowed annotation that can be used like this -

@RolesAllowed("ROLE_ADMIN") public Poll createPoll() {}

@PreAuthorize("isAnonymous()") public boolean isUsernameAvailable() {} @PreAuthorize("hasRole('USER')") public Poll createPoll() {}

3. WebSecurityConfigurerAdapter

This class implements Spring Security’s WebSecurityConfigurer interface. It provides default security configurations and allows other classes to extend it and customize the security configurations by overriding its methods.

Our SecurityConfig class extends WebSecurityConfigurerAdapter and overrides some of its methods to provide custom security configurations.

4. CustomUserDetailsService

To authenticate a User or perform various role-based checks, Spring security needs to load users details somehow.

For this purpose, It consists of an interface called UserDetailsService which has a single method that loads a user based on username-

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

We’ll define a CustomUserDetailsService that implements UserDetailsService interface and provides the implementation for loadUserByUsername() method.

Note that, the loadUserByUsername() method returns a UserDetails object that Spring Security uses for performing various authentication and role based validations.

In our implementation, We’ll also define a custom UserPrincipal class that will implement UserDetails interface, and return the UserPrincipal object from loadUserByUsername() method.

5. JwtAuthenticationEntryPoint

This class is used to return a 401 unauthorized error to clients that try to access a protected resource without proper authentication. It implements Spring Security’s AuthenticationEntryPoint interface.

6. JwtAuthenticationFilter

We’ll use JWTAuthenticationFilter to implement a filter that -

  • reads JWT authentication token from the Authorization header of all the requests
  • validates the token
  • loads the user details associated with that token.
  • Sets the user details in Spring Security’s SecurityContext. Spring Security uses the user details to perform authorization checks. We can also access the user details stored in the SecurityContext in our controllers to perform our business logic.

7. AuthenticationManagerBuilder and AuthenticationManager

AuthenticationManagerBuilder is used to create an AuthenticationManager instance which is the main Spring Security interface for authenticating a user.

You can use AuthenticationManagerBuilder to build in-memory authentication, LDAP authentication, JDBC authentication, or add your custom authentication provider.

In our example, we’ve provided our customUserDetailsService and a passwordEncoder to build the AuthenticationManager.

We’ll use the configured AuthenticationManager to authenticate a user in the login API.

8. HttpSecurity configurations

The HttpSecurity configurations are used to configure security functionalities like csrf, sessionManagement, and add rules to protect resources based on various conditions.

In our example, we’re permitting access to static resources and few other public APIs to everyone and restricting access to other APIs to authenticated users only.

We’ve also added the JWTAuthenticationEntryPoint and the custom JWTAuthenticationFilter in the HttpSecurity configuration.

Creating Custom Spring Security Classes, Filters, and Annotations

In the previous section, we configured spring security with many custom classes and filters. In this section, we’ll define those classes one by one.

All the following custom security related classes will go inside a package named com.example.poll.security.

1. Custom Spring Security AuthenticationEntryPoint

The first spring security related class that we’ll define is JwtAuthenticationEntryPoint. It implements AuthenticationEntryPoint interface and provides the implementation for its commence() method. This method is called whenever an exception is thrown due to an unauthenticated user trying to access a resource that requires authentication.

In this case, we’ll simply respond with a 401 error containing the exception message.

package com.example.polls.security;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

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

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
logger.error("Responding with unauthorized error. Message - {}", e.getMessage());
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
}
}

2. Custom Spring Security UserDetails

Next, Let’s define our custom UserDetails class called UserPrincipal. This is the class whose instances will be returned from our custom UserDetailsService. Spring Security will use the information stored in the UserPrincipal object to perform authentication and authorization.

Here is the complete UserPrincipal class -

package com.example.polls.security;
import com.example.polls.model.User;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class UserPrincipal implements UserDetails {
private Long id;
    private String name;
    private String username;
    @JsonIgnore
private String email;
    @JsonIgnore
private String password;
    private Collection<? extends GrantedAuthority> authorities;
    public UserPrincipal(Long id, String name, String username, String email, String password, Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.name = name;
this.username = username;
this.email = email;
this.password = password;
this.authorities = authorities;
}
    public static UserPrincipal create(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream().map(role ->
new SimpleGrantedAuthority(role.getName().name())
).collect(Collectors.toList());
        return new UserPrincipal(
user.getId(),
user.getName(),
user.getUsername(),
user.getEmail(),
user.getPassword(),
authorities
);
}
    public Long getId() {
return id;
}
    public String getName() {
return name;
}
    public String getEmail() {
return email;
}
    @Override
public String getUsername() {
return username;
}
    @Override
public String getPassword() {
return password;
}
    @Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
    @Override
public boolean isAccountNonExpired() {
return true;
}
    @Override
public boolean isAccountNonLocked() {
return true;
}
    @Override
public boolean isCredentialsNonExpired() {
return true;
}
    @Override
public boolean isEnabled() {
return true;
}
    @Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserPrincipal that = (UserPrincipal) o;
return Objects.equals(id, that.id);
}
    @Override
public int hashCode() {
        return Objects.hash(id);
}
}

3. Custom Spring Security UserDetailsService

Now let’s define the custom UserDetailsService which loads a user’s data given its username -

package com.example.polls.security;
import com.example.polls.model.User;
import com.example.polls.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
UserRepository userRepository;
    @Override
@Transactional
public UserDetails loadUserByUsername(String usernameOrEmail)
throws UsernameNotFoundException {
// Let people login with either username or email
User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
.orElseThrow(() ->
new UsernameNotFoundException("User not found with username or email : " + usernameOrEmail)
);
        return UserPrincipal.create(user);
}
    // This method is used by JWTAuthenticationFilter
@Transactional
public UserDetails loadUserById(Long id) {
User user = userRepository.findById(id).orElseThrow(
() -> new UsernameNotFoundException("User not found with id : " + id)
);
        return UserPrincipal.create(user);
}
}

The first method loadUserByUsername() is used by Spring security. Notice the use of findByUsernameOrEmail method. This allows users to log in using either username or email.

The second method loadUserById() will be used by JWTAuthenticationFilter that we’ll define shortly.

4. Utility class for generating and verifying JWT

The following utility class will be used for generating a JWT after a user logs in successfully, and validating the JWT sent in the Authorization header of the requests -

package com.example.polls.security;

import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.util.Date;

@Component
public class JwtTokenProvider {

private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);

@Value("${app.jwtSecret}")
private String jwtSecret;

@Value("${app.jwtExpirationInMs}")
private int jwtExpirationInMs;

public String generateToken(Authentication authentication) {

UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();

Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);

return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}

public Long getUserIdFromJWT(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();

return Long.parseLong(claims.getSubject());
}

public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException ex) {
logger.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
logger.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
logger.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
logger.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
logger.error("JWT claims string is empty.");
}
return false;
}
}

The utility class reads the JWT secret and expiration time from properties.

Let’s add the jwtSecret and jwtExpirationInMs properties in the application.properties file -

JWT Properties

## App Properties
app.jwtSecret= JWTSuperSecretKey
app.jwtExpirationInMs = 604800000

5. Custom Spring Security AuthenticationFilter

Finally, Let’s write the JWTAuthenticationFilter to get the JWT token from the request, validate it, load the user associated with the token, and pass it to Spring Security -

package com.example.polls.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
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;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
private JwtTokenProvider tokenProvider;
    @Autowired
private CustomUserDetailsService customUserDetailsService;
    private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
    @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
            if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromJWT(jwt);
                UserDetails userDetails = customUserDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
        filterChain.doFilter(request, response);
}
    private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}

In the above filter, We’re first parsing the JWT retrieved from the Authorization header of the request and obtaining the user’s Id. After that, We’re loading the user’s details from the database and setting the authentication inside spring security’s context.

Note that, the database hit in the above filter is optional. You could also encode the user’s username and roles inside JWT claims and create the UserDetails object by parsing those claims from the JWT. That would avoid the database hit.

However, Loading the current details of the user from the database might still be helpful. For example, you might wanna disallow login with this JWT if the user’s role has changed, or the user has updated his password after the creation of this JWT.

6. Custom annotation to access currently logged in user

Spring security provides an annotation called @AuthenticationPrincipal to access the currently authenticated user in the controllers.

The following CurrentUser annotation is a wrapper around @AuthenticationPrincipal annotation.

package com.example.polls.security;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import java.lang.annotation.*;

@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal
public @interface CurrentUser {

}

We’ve created a meta-annotation so that we don’t get too much tied up of with Spring Security related annotations everywhere in our project. This reduces the dependency on Spring Security. So if we decide to remove Spring Security from our project, we can easily do it by simply changing the CurrentUser annotation-

Writing the Login and Signup APIs

All right folks! We’re done with all the security configurations that were required. It’s time to finally write the login and signup APIs.

But, Before defining the APIs, we’ll need to define the request and response payloads that the APIs will use. So let’s define these payloads first.

All the request and response payloads will go inside a package named com.example.polls.payload.

Request Payloads

1. LoginRequest

package com.example.polls.payload;
import javax.validation.constraints.NotBlank;
public class LoginRequest {
@NotBlank
private String usernameOrEmail;
    @NotBlank
private String password;
    public String getUsernameOrEmail() {
return usernameOrEmail;
}
    public void setUsernameOrEmail(String usernameOrEmail) {
this.usernameOrEmail = usernameOrEmail;
}
    public String getPassword() {
return password;
}
    public void setPassword(String password) {
this.password = password;
}
}

2. SignUpRequest

package com.example.polls.payload;
import javax.validation.constraints.*;
public class SignUpRequest {
@NotBlank
@Size(min = 4, max = 40)
private String name;
    @NotBlank
@Size(min = 3, max = 15)
private String username;
    @NotBlank
@Size(max = 40)
@Email
private String email;
    @NotBlank
@Size(min = 6, max = 20)
private String password;
    public String getName() {
return name;
}
    public void setName(String name) {
this.name = name;
}
    public String getUsername() {
return username;
}
    public void setUsername(String username) {
this.username = username;
}
    public String getEmail() {
return email;
}
    public void setEmail(String email) {
this.email = email;
}
    public String getPassword() {
return password;
}
    public void setPassword(String password) {
this.password = password;
}
}

Response Payloads

1. JwtAuthenticationResponse

package com.example.polls.payload;
public class JwtAuthenticationResponse {
private String accessToken;
private String tokenType = "Bearer";
    public JwtAuthenticationResponse(String accessToken) {
this.accessToken = accessToken;
}
    public String getAccessToken() {
return accessToken;
}
    public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
    public String getTokenType() {
return tokenType;
}
    public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
}

2. ApiResponse

package com.example.polls.payload;
public class ApiResponse {
private Boolean success;
private String message;
    public ApiResponse(Boolean success, String message) {
this.success = success;
this.message = message;
}
    public Boolean getSuccess() {
return success;
}
    public void setSuccess(Boolean success) {
this.success = success;
}
    public String getMessage() {
return message;
}
    public void setMessage(String message) {
this.message = message;
}
}

Custom Business Exceptions

The APIs will throw exceptions if the request is not valid or some unexpected situation occurs.

We would also want to respond with different HTTP status codes for different types of exceptions.

Let’s define these exceptions along with the corresponding @ResponseStatus (All the exception classes will go inside a package named com.example.polls.exception) -

1. AppException

package com.example.polls.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public class AppException extends RuntimeException {
public AppException(String message) {
super(message);
}
    public AppException(String message, Throwable cause) {
super(message, cause);
}
}

2. BadRequestException

package com.example.polls.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException {
    public BadRequestException(String message) {
super(message);
}
    public BadRequestException(String message, Throwable cause) {
super(message, cause);
}
}

3. ResourceNotFoundException

package com.example.polls.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
private String resourceName;
private String fieldName;
private Object fieldValue;
    public ResourceNotFoundException( String resourceName, String fieldName, Object fieldValue) {
super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue));
this.resourceName = resourceName;
this.fieldName = fieldName;
this.fieldValue = fieldValue;
}
    public String getResourceName() {
return resourceName;
}
    public String getFieldName() {
return fieldName;
}
    public Object getFieldValue() {
return fieldValue;
}
}

Authentication Controller

Finally, Here is the complete code for AuthController that contains APIs for login and signup (All the controllers in our project will go inside a package named com.example.polls.controller) -

package com.example.polls.controller;
import com.example.polls.exception.AppException;
import com.example.polls.model.Role;
import com.example.polls.model.RoleName;
import com.example.polls.model.User;
import com.example.polls.payload.ApiResponse;
import com.example.polls.payload.JwtAuthenticationResponse;
import com.example.polls.payload.LoginRequest;
import com.example.polls.payload.SignUpRequest;
import com.example.polls.repository.RoleRepository;
import com.example.polls.repository.UserRepository;
import com.example.polls.security.JwtTokenProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import javax.validation.Valid;
import java.net.URI;
import java.util.Collections;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    @Autowired
AuthenticationManager authenticationManager;
    @Autowired
UserRepository userRepository;
    @Autowired
RoleRepository roleRepository;
    @Autowired
PasswordEncoder passwordEncoder;
    @Autowired
JwtTokenProvider tokenProvider;
    @PostMapping("/signin")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
        Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsernameOrEmail(),
loginRequest.getPassword()
)
);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        String jwt = tokenProvider.generateToken(authentication);
return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
}
    @PostMapping("/signup")
public ResponseEntity<?> registerUser(@Valid @RequestBody SignUpRequest signUpRequest) {
if(userRepository.existsByUsername(signUpRequest.getUsername())) {
return new ResponseEntity(new ApiResponse(false, "Username is already taken!"),
HttpStatus.BAD_REQUEST);
}
        if(userRepository.existsByEmail(signUpRequest.getEmail())) {
return new ResponseEntity(new ApiResponse(false, "Email Address already in use!"),
HttpStatus.BAD_REQUEST);
}
        // Creating user's account
User user = new User(signUpRequest.getName(), signUpRequest.getUsername(),
signUpRequest.getEmail(), signUpRequest.getPassword());
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        Role userRole = roleRepository.findByName(RoleName.ROLE_USER)
.orElseThrow(() -> new AppException("User Role not set."));
        user.setRoles(Collections.singleton(userRole));
        User result = userRepository.save(user);
        URI location = ServletUriComponentsBuilder
.fromCurrentContextPath().path("/api/users/{username}")
.buildAndExpand(result.getUsername()).toUri();
        return ResponseEntity.created(location).body(new ApiResponse(true, "User registered successfully"));
}
}

Enabling CORS

We’ll be accessing the APIs from the react client that will run on its own development server. To allow cross origin requests from the react client, create the following WebMvcConfig class inside com.example.polls.config package -

package com.example.polls.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    private final long MAX_AGE_SECS = 3600;
    @Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("HEAD", "OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE")
.maxAge(MAX_AGE_SECS);
}
}

Exploring the current project setup & Running the Application

If you have followed along and defined all the classes presented in this article, then your project’s directory structure should look like this-

You can run the application by typing the following command from the root directory of your project -

mvn spring-boot:run

Testing the Login and Signup APIs

Let’s now test the login and signup APIs from Postman.

SignUp

Login

Calling Protected APIs

Once you’ve obtained the access token using the login API, you can call any protected API by passing the accessToken in the Authorization header of the request like so -

Authorization: Bearer <accessToken>

The JwtAuthenticationFilter will read the accessToken from the header, verify it, and allow/deny access to the API.

Then, We’ll build Rest APIs to create and retrieve Polls, vote for a choice in a Poll, get a user’s profile and much more.

Before building the Rest APIs, we’ll need to create the domain models for Poll, Choice and Vote.

We would want to include information about who created or updated a poll in the Poll model, and automatically populate this information based on the currently logged in user.

Auditing Model and Configurations

To achieve user auditing, let’s define an auditing model called UserDateAudit which extends the DateAudit model that we defined in the first part.

It includes createdBy and updatedBy fields.

UserDateAudit model

Create the following UserDateAudit class inside com.example.polls.model.audit package -

package com.example.polls.model.audit;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;

import javax.persistence.Column;
import javax.persistence.MappedSuperclass;

@MappedSuperclass
@JsonIgnoreProperties(
value = {"createdBy", "updatedBy"},
allowGetters = true
)
public abstract class UserDateAudit extends DateAudit {
@CreatedBy
@Column(updatable = false)
private Long createdBy;

@LastModifiedBy
private Long updatedBy;

public Long getCreatedBy() {
return createdBy;
}

public void setCreatedBy(Long createdBy) {
this.createdBy = createdBy;
}

public Long getUpdatedBy() {
return updatedBy;
}

public void setUpdatedBy(Long updatedBy) {
this.updatedBy = updatedBy;
}
}

Auditing configuration

Now, to automatically populate the createdBy and updatedBy fields, we need to make the following modifications to the AuditingConfig class -

package com.example.polls.config;
import com.example.polls.security.UserPrincipal;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Optional;
@Configuration
@EnableJpaAuditing
public class AuditingConfig {
    @Bean
public AuditorAware<Long> auditorProvider() {
return new SpringSecurityAuditAwareImpl();
}
}
class SpringSecurityAuditAwareImpl implements AuditorAware<Long> {
    @Override
public Optional<Long> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null ||
!authentication.isAuthenticated() ||
authentication instanceof AnonymousAuthenticationToken) {
return Optional.empty();
}
        UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();

return Optional.ofNullable(userPrincipal.getId());
}
}

Business models

1. Poll model

A poll has an id, a question, a list of choices and an expirationDateTime. Following is the complete Poll class -

package com.example.polls.model;
import com.example.polls.model.audit.UserDateAudit;
import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "polls")
public class Poll extends UserDateAudit {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
    @NotBlank
@Size(max = 140)
private String question;
    @OneToMany(
mappedBy = "poll",
cascade = CascadeType.ALL,
fetch = FetchType.EAGER,
orphanRemoval = true
)
@Size(min = 2, max = 6)
@Fetch(FetchMode.SELECT)
@BatchSize(size = 30)
private List<Choice> choices = new ArrayList<>();
    @NotNull
private Instant expirationDateTime;
    public Long getId() {
return id;
}
    public void setId(Long id) {
this.id = id;
}
    public String getQuestion() {
return question;
}
    public void setQuestion(String question) {
this.question = question;
}
    public List<Choice> getChoices() {
return choices;
}
    public void setChoices(List<Choice> choices) {
this.choices = choices;
}
    public Instant getExpirationDateTime() {
return expirationDateTime;
}
    public void setExpirationDateTime(Instant expirationDateTime) {
this.expirationDateTime = expirationDateTime;
}
    public void addChoice(Choice choice) {
choices.add(choice);
choice.setPoll(this);
}
    public void removeChoice(Choice choice) {
choices.remove(choice);
choice.setPoll(null);
}
}

2. Choice model

Every Poll choice has an id, a text and is related to a Poll via a foreign key relationship. Here is the complete Choice class -

package com.example.polls.model;
import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.util.Objects;
@Entity
@Table(name = "choices")
public class Choice {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
    @NotBlank
@Size(max = 40)
private String text;
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "poll_id", nullable = false)
private Poll poll;
    public Choice() {
    }
    public Choice(String text) {
this.text = text;
}
    public Long getId() {
return id;
}
    public void setId(Long id) {
this.id = id;
}
    public String getText() {
return text;
}
    public void setText(String text) {
this.text = text;
}
    public Poll getPoll() {
return poll;
}
    public void setPoll(Poll poll) {
this.poll = poll;
}
    @Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Choice choice = (Choice) o;
return Objects.equals(id, choice.id);
}
    @Override
public int hashCode() {
return Objects.hash(id);
}
}

3. Vote model

The Vote class contains information about which user voted for which choice in a poll. Following is the complete Vote class -

package com.example.polls.model;
import com.example.polls.model.audit.DateAudit;
import javax.persistence.*;
@Entity
@Table(name = "votes", uniqueConstraints = {
@UniqueConstraint(columnNames = {
"poll_id",
"user_id"
})
})
public class Vote extends DateAudit {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "poll_id", nullable = false)
private Poll poll;
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "choice_id", nullable = false)
private Choice choice;
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false)
private User user;
    public Long getId() {
return id;
}
    public void setId(Long id) {
this.id = id;
}
    public Poll getPoll() {
return poll;
}
    public void setPoll(Poll poll) {
this.poll = poll;
}
    public Choice getChoice() {
return choice;
}
    public void setChoice(Choice choice) {
this.choice = choice;
}
    public User getUser() {
return user;
}
    public void setUser(User user) {
this.user = user;
}
}

Repositories

Let’s now define the repositories to access Poll, Choice and Vote data from the database.

1. PollRepository

package com.example.polls.repository;
import com.example.polls.model.Poll;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface PollRepository extends JpaRepository<Poll, Long> {
Optional<Poll> findById(Long pollId);
    Page<Poll> findByCreatedBy(Long userId, Pageable pageable);
    long countByCreatedBy(Long userId);
    List<Poll> findByIdIn(List<Long> pollIds);
    List<Poll> findByIdIn(List<Long> pollIds, Sort sort);
}

2. VoteRepository

package com.example.polls.repository;
import com.example.polls.model.ChoiceVoteCount;
import com.example.polls.model.Vote;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface VoteRepository extends JpaRepository<Vote, Long> {
@Query("SELECT NEW com.example.polls.model.ChoiceVoteCount(v.choice.id, count(v.id)) FROM Vote v WHERE v.poll.id in :pollIds GROUP BY v.choice.id")
List<ChoiceVoteCount> countByPollIdInGroupByChoiceId(@Param("pollIds") List<Long> pollIds);
    @Query("SELECT NEW com.example.polls.model.ChoiceVoteCount(v.choice.id, count(v.id)) FROM Vote v WHERE v.poll.id = :pollId GROUP BY v.choice.id")
List<ChoiceVoteCount> countByPollIdGroupByChoiceId(@Param("pollId") Long pollId);
    @Query("SELECT v FROM Vote v where v.user.id = :userId and v.poll.id in :pollIds")
List<Vote> findByUserIdAndPollIdIn(@Param("userId") Long userId, @Param("pollIds") List<Long> pollIds);
    @Query("SELECT v FROM Vote v where v.user.id = :userId and v.poll.id = :pollId")
Vote findByUserIdAndPollId(@Param("userId") Long userId, @Param("pollId") Long pollId);
    @Query("SELECT COUNT(v.id) from Vote v where v.user.id = :userId")
long countByUserId(@Param("userId") Long userId);
    @Query("SELECT v.poll.id FROM Vote v WHERE v.user.id = :userId")
Page<Long> findVotedPollIdsByUserId(@Param("userId") Long userId, Pageable pageable);
}

All of the methods in VoteRepository have a custom query with @Query annotation. I’ve used custom queries because -

Note that, we’re using JPQL constructor expression in some of the queries to return the query result in the form of a custom class called ChoiceVoteCount.

ChoiceVoteCount domain class

The ChoiceVoteCount class is used in VoteRepository to return custom results from the query. Here is the complete ChoiceVoteCount class -

package com.example.polls.model;
public class ChoiceVoteCount {
private Long choiceId;
private Long voteCount;
    public ChoiceVoteCount(Long choiceId, Long voteCount) {
this.choiceId = choiceId;
this.voteCount = voteCount;
}
    public Long getChoiceId() {
return choiceId;
}
    public void setChoiceId(Long choiceId) {
this.choiceId = choiceId;
}
    public Long getVoteCount() {
return voteCount;
}
    public void setVoteCount(Long voteCount) {
this.voteCount = voteCount;
}
}

Defining the Rest APIs

Finally, Let’s write the APIs to create a poll, get all polls, vote for a choice in a poll, get a user’s profile, get polls created by a user etc.

Note that, the rest APIs will accept custom payloads in the request, and they will also return custom responses to the clients that include either selected information or additional information.

Following are the request and response payloads that will be used in the rest APIs (All the payloads go inside a package named com.example.polls.payload)-

Request Payloads

1. PollRequest

package com.example.polls.payload;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.List;
public class PollRequest {
@NotBlank
@Size(max = 140)
private String question;
    @NotNull
@Size(min = 2, max = 6)
@Valid
private List<ChoiceRequest> choices;
    @NotNull
@Valid
private PollLength pollLength;
    public String getQuestion() {
return question;
}
    public void setQuestion(String question) {
this.question = question;
}
    public List<ChoiceRequest> getChoices() {
return choices;
}
    public void setChoices(List<ChoiceRequest> choices) {
this.choices = choices;
}
    public PollLength getPollLength() {
return pollLength;
}
    public void setPollLength(PollLength pollLength) {
this.pollLength = pollLength;
}
}

2. ChoiceRequest

package com.example.polls.payload;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
public class ChoiceRequest {
@NotBlank
@Size(max = 40)
private String text;
    public String getText() {
return text;
}
    public void setText(String text) {
this.text = text;
}
}

3. PollLength

package com.example.polls.payload;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
public class PollLength {
@NotNull
@Max(7)
private Integer days;
    @NotNull
@Max(23)
private Integer hours;
    public int getDays() {
return days;
}
    public void setDays(int days) {
this.days = days;
}
    public int getHours() {
return hours;
}
    public void setHours(int hours) {
this.hours = hours;
}
}

4. VoteRequest

package com.example.polls.payload;
import javax.validation.constraints.NotNull;
public class VoteRequest {
@NotNull
private Long choiceId;
    public Long getChoiceId() {
return choiceId;
}
    public void setChoiceId(Long choiceId) {
this.choiceId = choiceId;
}
}

Response Payloads

1. UserSummary

package com.example.polls.payload;
public class UserSummary {
private Long id;
private String username;
private String name;
    public UserSummary(Long id, String username, String name) {
this.id = id;
this.username = username;
this.name = name;
}
    public Long getId() {
return id;
}
    public void setId(Long id) {
this.id = id;
}
    public String getUsername() {
return username;
}
    public void setUsername(String username) {
this.username = username;
}
    public String getName() {
return name;
}
    public void setName(String name) {
this.name = name;
}
}

2. UserIdentityAvailability

package com.example.polls.payload;
public class UserIdentityAvailability {
private Boolean available;
    public UserIdentityAvailability(Boolean available) {
this.available = available;
}
    public Boolean getAvailable() {
return available;
}
    public void setAvailable(Boolean available) {
this.available = available;
}
}

3. UserProfile

package com.example.polls.payload;
import java.time.Instant;
public class UserProfile {
private Long id;
private String username;
private String name;
private Instant joinedAt;
private Long pollCount;
private Long voteCount;
    public UserProfile(Long id, String username, String name, Instant joinedAt, Long pollCount, Long voteCount) {
this.id = id;
this.username = username;
this.name = name;
this.joinedAt = joinedAt;
this.pollCount = pollCount;
this.voteCount = voteCount;
}
    public Long getId() {
return id;
}
    public void setId(Long id) {
this.id = id;
}
    public String getUsername() {
return username;
}
    public void setUsername(String username) {
this.username = username;
}
    public String getName() {
return name;
}
    public void setName(String name) {
this.name = name;
}
    public Instant getJoinedAt() {
return joinedAt;
}
    public void setJoinedAt(Instant joinedAt) {
this.joinedAt = joinedAt;
}
    public Long getPollCount() {
return pollCount;
}
    public void setPollCount(Long pollCount) {
this.pollCount = pollCount;
}
    public Long getVoteCount() {
return voteCount;
}
    public void setVoteCount(Long voteCount) {
this.voteCount = voteCount;
}
}

4. PollResponse

package com.example.polls.payload;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.time.Instant;
import java.util.List;
public class PollResponse {
private Long id;
private String question;
private List<ChoiceResponse> choices;
private UserSummary createdBy;
private Instant creationDateTime;
private Instant expirationDateTime;
private Boolean isExpired;
    @JsonInclude(JsonInclude.Include.NON_NULL)
private Long selectedChoice;
private Long totalVotes;
    public Long getId() {
return id;
}
    public void setId(Long id) {
this.id = id;
}
    public String getQuestion() {
return question;
}
    public void setQuestion(String question) {
this.question = question;
}
    public List<ChoiceResponse> getChoices() {
return choices;
}
    public void setChoices(List<ChoiceResponse> choices) {
this.choices = choices;
}
    public UserSummary getCreatedBy() {
return createdBy;
}
    public void setCreatedBy(UserSummary createdBy) {
this.createdBy = createdBy;
}

    public Instant getCreationDateTime() {
return creationDateTime;
}
    public void setCreationDateTime(Instant creationDateTime) {
this.creationDateTime = creationDateTime;
}
    public Instant getExpirationDateTime() {
return expirationDateTime;
}
    public void setExpirationDateTime(Instant expirationDateTime) {
this.expirationDateTime = expirationDateTime;
}
    public Boolean getExpired() {
return isExpired;
}
    public void setExpired(Boolean expired) {
isExpired = expired;
}
    public Long getSelectedChoice() {
return selectedChoice;
}
    public void setSelectedChoice(Long selectedChoice) {
this.selectedChoice = selectedChoice;
}
    public Long getTotalVotes() {
return totalVotes;
}
    public void setTotalVotes(Long totalVotes) {
this.totalVotes = totalVotes;
}
}

5. ChoiceResponse

package com.example.polls.payload;
public class ChoiceResponse {
private long id;
private String text;
private long voteCount;
    public long getId() {
return id;
}
    public void setId(long id) {
this.id = id;
}
    public String getText() {
return text;
}
    public void setText(String text) {
this.text = text;
}
    public long getVoteCount() {
return voteCount;
}
    public void setVoteCount(long voteCount) {
this.voteCount = voteCount;
}
}

6. PagedResponse

package com.example.polls.payload;
import java.util.List;
public class PagedResponse<T> {
    private List<T> content;
private int page;
private int size;
private long totalElements;
private int totalPages;
private boolean last;
    public PagedResponse() {
    }
    public PagedResponse(List<T> content, int page, int size, long totalElements, int totalPages, boolean last) {
this.content = content;
this.page = page;
this.size = size;
this.totalElements = totalElements;
this.totalPages = totalPages;
this.last = last;
}
    public List<T> getContent() {
return content;
}
    public void setContent(List<T> content) {
this.content = content;
}
    public int getPage() {
return page;
}
    public void setPage(int page) {
this.page = page;
}
    public int getSize() {
return size;
}
    public void setSize(int size) {
this.size = size;
}
    public long getTotalElements() {
return totalElements;
}
    public void setTotalElements(long totalElements) {
this.totalElements = totalElements;
}
    public int getTotalPages() {
return totalPages;
}
    public void setTotalPages(int totalPages) {
this.totalPages = totalPages;
}
    public boolean isLast() {
return last;
}
    public void setLast(boolean last) {
this.last = last;
}
}

Util classes used by controllers and services

Apart from the Request and Response payloads, all the rest controllers and services will also be using some utility classes.

Following are few utility classes that are used by our controllers and services -

1. AppConstants

package com.example.polls.util;
public interface AppConstants {
String DEFAULT_PAGE_NUMBER = "0";
String DEFAULT_PAGE_SIZE = "30";
    int MAX_PAGE_SIZE = 50;
}

2. ModelMapper

package com.example.polls.util;
import com.example.polls.model.Poll;
import com.example.polls.model.User;
import com.example.polls.payload.ChoiceResponse;
import com.example.polls.payload.PollResponse;
import com.example.polls.payload.UserSummary;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class ModelMapper {
    public static PollResponse mapPollToPollResponse(Poll poll, Map<Long, Long> choiceVotesMap, User creator, Long userVote) {
PollResponse pollResponse = new PollResponse();
pollResponse.setId(poll.getId());
pollResponse.setQuestion(poll.getQuestion());
pollResponse.setCreationDateTime(poll.getCreatedAt());
pollResponse.setExpirationDateTime(poll.getExpirationDateTime());
Instant now = Instant.now();
pollResponse.setExpired(poll.getExpirationDateTime().isBefore(now));
        List<ChoiceResponse> choiceResponses = poll.getChoices().stream().map(choice -> {
ChoiceResponse choiceResponse = new ChoiceResponse();
choiceResponse.setId(choice.getId());
choiceResponse.setText(choice.getText());
            if(choiceVotesMap.containsKey(choice.getId())) {
choiceResponse.setVoteCount(choiceVotesMap.get(choice.getId()));
} else {
choiceResponse.setVoteCount(0);
}
return choiceResponse;
}).collect(Collectors.toList());
        pollResponse.setChoices(choiceResponses);
UserSummary creatorSummary = new UserSummary(creator.getId(), creator.getUsername(), creator.getName());
pollResponse.setCreatedBy(creatorSummary);
        if(userVote != null) {
pollResponse.setSelectedChoice(userVote);
}
        long totalVotes = pollResponse.getChoices().stream().mapToLong(ChoiceResponse::getVoteCount).sum();
pollResponse.setTotalVotes(totalVotes);
        return pollResponse;
}
}

We’ll be mapping the Poll entity to a PollResponse payload which contains a bunch of information like Poll’s creator name, Vote counts of each choice in the Poll, the choice that the currently logged in user has voted for, is the Poll expired etc. All these information will be used in front-end client for presentation.

Writing the Rest Controllers

We’re all setup for writing the Rest APIs in the controllers. All the controllers will go inside a package named com.example.polls.controller.

1. PollController

In PollController, we’ll write the Rest APIs to -

  • Create a Poll.
  • Get a paginated list of polls sorted by their creation time.
  • Get a Poll by pollId.
  • Vote for a choice in a poll.

The PollController also uses a service called PollService for validating and processing some of the requests. We’ll define PollService in the next section.

package com.example.polls.controller;

import com.example.polls.model.*;
import com.example.polls.payload.*;
import com.example.polls.repository.PollRepository;
import com.example.polls.repository.UserRepository;
import com.example.polls.repository.VoteRepository;
import com.example.polls.security.CurrentUser;
import com.example.polls.security.UserPrincipal;
import com.example.polls.service.PollService;
import com.example.polls.util.AppConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import javax.validation.Valid;
import java.net.URI;

@RestController
@RequestMapping("/api/polls")
public class PollController {

@Autowired
private PollRepository pollRepository;

@Autowired
private VoteRepository voteRepository;

@Autowired
private UserRepository userRepository;

@Autowired
private PollService pollService;

private static final Logger logger = LoggerFactory.getLogger(PollController.class);

@GetMapping
public PagedResponse<PollResponse> getPolls(@CurrentUser UserPrincipal currentUser,
@RequestParam(value = "page", defaultValue = AppConstants.DEFAULT_PAGE_NUMBER) int page,
@RequestParam(value = "size", defaultValue = AppConstants.DEFAULT_PAGE_SIZE) int size) {
return pollService.getAllPolls(currentUser, page, size);
}

@PostMapping
@PreAuthorize("hasRole('USER')")
public ResponseEntity<?> createPoll(@Valid @RequestBody PollRequest pollRequest) {
Poll poll = pollService.createPoll(pollRequest);

URI location = ServletUriComponentsBuilder
.fromCurrentRequest().path("/{pollId}")
.buildAndExpand(poll.getId()).toUri();

return ResponseEntity.created(location)
.body(new ApiResponse(true, "Poll Created Successfully"));
}

@GetMapping("/{pollId}")
public PollResponse getPollById(@CurrentUser UserPrincipal currentUser,
@PathVariable Long pollId) {
return pollService.getPollById(pollId, currentUser);
}

@PostMapping("/{pollId}/votes")
@PreAuthorize("hasRole('USER')")
public PollResponse castVote(@CurrentUser UserPrincipal currentUser,
@PathVariable Long pollId,
@Valid @RequestBody VoteRequest voteRequest) {
return pollService.castVoteAndGetUpdatedPoll(pollId, voteRequest, currentUser);
}
}

2. UserController

In UserController, We’ll be writing APIs to -

  • Get the currently logged in user.
  • Check if a username is available for registration.
  • Check if an email is available for registration.
  • Get the public profile of a user.
  • Get a paginated list of polls created by a given user.
  • Get a paginated list of polls in which a given user has voted.
package com.example.polls.controller;

import com.example.polls.exception.ResourceNotFoundException;
import com.example.polls.model.User;
import com.example.polls.payload.*;
import com.example.polls.repository.PollRepository;
import com.example.polls.repository.UserRepository;
import com.example.polls.repository.VoteRepository;
import com.example.polls.security.UserPrincipal;
import com.example.polls.service.PollService;
import com.example.polls.security.CurrentUser;
import com.example.polls.util.AppConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class UserController {

@Autowired
private UserRepository userRepository;

@Autowired
private PollRepository pollRepository;

@Autowired
private VoteRepository voteRepository;

@Autowired
private PollService pollService;

private static final Logger logger = LoggerFactory.getLogger(UserController.class);

@GetMapping("/user/me")
@PreAuthorize("hasRole('USER')")
public UserSummary getCurrentUser(@CurrentUser UserPrincipal currentUser) {
UserSummary userSummary = new UserSummary(currentUser.getId(), currentUser.getUsername(), currentUser.getName());
return userSummary;
}

@GetMapping("/user/checkUsernameAvailability")
public UserIdentityAvailability checkUsernameAvailability(@RequestParam(value = "username") String username) {
Boolean isAvailable = !userRepository.existsByUsername(username);
return new UserIdentityAvailability(isAvailable);
}

@GetMapping("/user/checkEmailAvailability")
public UserIdentityAvailability checkEmailAvailability(@RequestParam(value = "email") String email) {
Boolean isAvailable = !userRepository.existsByEmail(email);
return new UserIdentityAvailability(isAvailable);
}

@GetMapping("/users/{username}")
public UserProfile getUserProfile(@PathVariable(value = "username") String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new ResourceNotFoundException("User", "username", username));

long pollCount = pollRepository.countByCreatedBy(user.getId());
long voteCount = voteRepository.countByUserId(user.getId());

UserProfile userProfile = new UserProfile(user.getId(), user.getUsername(), user.getName(), user.getCreatedAt(), pollCount, voteCount);

return userProfile;
}

@GetMapping("/users/{username}/polls")
public PagedResponse<PollResponse> getPollsCreatedBy(@PathVariable(value = "username") String username,
@CurrentUser UserPrincipal currentUser,
@RequestParam(value = "page", defaultValue = AppConstants.DEFAULT_PAGE_NUMBER) int page,
@RequestParam(value = "size", defaultValue = AppConstants.DEFAULT_PAGE_SIZE) int size) {
return pollService.getPollsCreatedBy(username, currentUser, page, size);
}


@GetMapping("/users/{username}/votes")
public PagedResponse<PollResponse> getPollsVotedBy(@PathVariable(value = "username") String username,
@CurrentUser UserPrincipal currentUser,
@RequestParam(value = "page", defaultValue = AppConstants.DEFAULT_PAGE_NUMBER) int page,
@RequestParam(value = "size", defaultValue = AppConstants.DEFAULT_PAGE_SIZE) int size) {
return pollService.getPollsVotedBy(username, currentUser, page, size);
}

}

The Service layer — PollService

Both the controllers PollController and UserController use the PollService class to get the list of polls formatted in the form of PollResponse payloads that is returned to the clients.

Here is the complete code for PollService (All the services go inside a package named com.example.polls.service) -

package com.example.polls.service;
import com.example.polls.exception.BadRequestException;
import com.example.polls.exception.ResourceNotFoundException;
import com.example.polls.model.*;
import com.example.polls.payload.PagedResponse;
import com.example.polls.payload.PollRequest;
import com.example.polls.payload.PollResponse;
import com.example.polls.payload.VoteRequest;
import com.example.polls.repository.PollRepository;
import com.example.polls.repository.UserRepository;
import com.example.polls.repository.VoteRepository;
import com.example.polls.security.UserPrincipal;
import com.example.polls.util.AppConstants;
import com.example.polls.util.ModelMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
public class PollService {
    @Autowired
private PollRepository pollRepository;
    @Autowired
private VoteRepository voteRepository;
    @Autowired
private UserRepository userRepository;
    private static final Logger logger = LoggerFactory.getLogger(PollService.class);
    public PagedResponse<PollResponse> getAllPolls(UserPrincipal currentUser, int page, int size) {
validatePageNumberAndSize(page, size);
        // Retrieve Polls
Pageable pageable = PageRequest.of(page, size, Sort.Direction.DESC, "createdAt");
Page<Poll> polls = pollRepository.findAll(pageable);
        if(polls.getNumberOfElements() == 0) {
return new PagedResponse<>(Collections.emptyList(), polls.getNumber(),
polls.getSize(), polls.getTotalElements(), polls.getTotalPages(), polls.isLast());
}
        // Map Polls to PollResponses containing vote counts and poll creator details
List<Long> pollIds = polls.map(Poll::getId).getContent();
Map<Long, Long> choiceVoteCountMap = getChoiceVoteCountMap(pollIds);
Map<Long, Long> pollUserVoteMap = getPollUserVoteMap(currentUser, pollIds);
Map<Long, User> creatorMap = getPollCreatorMap(polls.getContent());
        List<PollResponse> pollResponses = polls.map(poll -> {
return ModelMapper.mapPollToPollResponse(poll,
choiceVoteCountMap,
creatorMap.get(poll.getCreatedBy()),
pollUserVoteMap == null ? null : pollUserVoteMap.getOrDefault(poll.getId(), null));
}).getContent();
        return new PagedResponse<>(pollResponses, polls.getNumber(),
polls.getSize(), polls.getTotalElements(), polls.getTotalPages(), polls.isLast());
}
    public PagedResponse<PollResponse> getPollsCreatedBy(String username, UserPrincipal currentUser, int page, int size) {
validatePageNumberAndSize(page, size);
        User user = userRepository.findByUsername(username)
.orElseThrow(() -> new ResourceNotFoundException("User", "username", username));
        // Retrieve all polls created by the given username
Pageable pageable = PageRequest.of(page, size, Sort.Direction.DESC, "createdAt");
Page<Poll> polls = pollRepository.findByCreatedBy(user.getId(), pageable);
        if (polls.getNumberOfElements() == 0) {
return new PagedResponse<>(Collections.emptyList(), polls.getNumber(),
polls.getSize(), polls.getTotalElements(), polls.getTotalPages(), polls.isLast());
}
        // Map Polls to PollResponses containing vote counts and poll creator details
List<Long> pollIds = polls.map(Poll::getId).getContent();
Map<Long, Long> choiceVoteCountMap = getChoiceVoteCountMap(pollIds);
Map<Long, Long> pollUserVoteMap = getPollUserVoteMap(currentUser, pollIds);
        List<PollResponse> pollResponses = polls.map(poll -> {
return ModelMapper.mapPollToPollResponse(poll,
choiceVoteCountMap,
user,
pollUserVoteMap == null ? null : pollUserVoteMap.getOrDefault(poll.getId(), null));
}).getContent();
        return new PagedResponse<>(pollResponses, polls.getNumber(),
polls.getSize(), polls.getTotalElements(), polls.getTotalPages(), polls.isLast());
}
    public PagedResponse<PollResponse> getPollsVotedBy(String username, UserPrincipal currentUser, int page, int size) {
validatePageNumberAndSize(page, size);
        User user = userRepository.findByUsername(username)
.orElseThrow(() -> new ResourceNotFoundException("User", "username", username));
        // Retrieve all pollIds in which the given username has voted
Pageable pageable = PageRequest.of(page, size, Sort.Direction.DESC, "createdAt");
Page<Long> userVotedPollIds = voteRepository.findVotedPollIdsByUserId(user.getId(), pageable);
        if (userVotedPollIds.getNumberOfElements() == 0) {
return new PagedResponse<>(Collections.emptyList(), userVotedPollIds.getNumber(),
userVotedPollIds.getSize(), userVotedPollIds.getTotalElements(),
userVotedPollIds.getTotalPages(), userVotedPollIds.isLast());
}
        // Retrieve all poll details from the voted pollIds.
List<Long> pollIds = userVotedPollIds.getContent();
        Sort sort = Sort.by(Sort.Direction.DESC, "createdAt");
List<Poll> polls = pollRepository.findByIdIn(pollIds, sort);
        // Map Polls to PollResponses containing vote counts and poll creator details
Map<Long, Long> choiceVoteCountMap = getChoiceVoteCountMap(pollIds);
Map<Long, Long> pollUserVoteMap = getPollUserVoteMap(currentUser, pollIds);
Map<Long, User> creatorMap = getPollCreatorMap(polls);
        List<PollResponse> pollResponses = polls.stream().map(poll -> {
return ModelMapper.mapPollToPollResponse(poll,
choiceVoteCountMap,
creatorMap.get(poll.getCreatedBy()),
pollUserVoteMap == null ? null : pollUserVoteMap.getOrDefault(poll.getId(), null));
}).collect(Collectors.toList());
        return new PagedResponse<>(pollResponses, userVotedPollIds.getNumber(), userVotedPollIds.getSize(), userVotedPollIds.getTotalElements(), userVotedPollIds.getTotalPages(), userVotedPollIds.isLast());
}

    public Poll createPoll(PollRequest pollRequest) {
Poll poll = new Poll();
poll.setQuestion(pollRequest.getQuestion());
        pollRequest.getChoices().forEach(choiceRequest -> {
poll.addChoice(new Choice(choiceRequest.getText()));
});
        Instant now = Instant.now();
Instant expirationDateTime = now.plus(Duration.ofDays(pollRequest.getPollLength().getDays()))
.plus(Duration.ofHours(pollRequest.getPollLength().getHours()));
        poll.setExpirationDateTime(expirationDateTime);
        return pollRepository.save(poll);
}
    public PollResponse getPollById(Long pollId, UserPrincipal currentUser) {
Poll poll = pollRepository.findById(pollId).orElseThrow(
() -> new ResourceNotFoundException("Poll", "id", pollId));
        // Retrieve Vote Counts of every choice belonging to the current poll
List<ChoiceVoteCount> votes = voteRepository.countByPollIdGroupByChoiceId(pollId);
        Map<Long, Long> choiceVotesMap = votes.stream()
.collect(Collectors.toMap(ChoiceVoteCount::getChoiceId, ChoiceVoteCount::getVoteCount));
        // Retrieve poll creator details
User creator = userRepository.findById(poll.getCreatedBy())
.orElseThrow(() -> new ResourceNotFoundException("User", "id", poll.getCreatedBy()));
        // Retrieve vote done by logged in user
Vote userVote = null;
if(currentUser != null) {
userVote = voteRepository.findByUserIdAndPollId(currentUser.getId(), pollId);
}
        return ModelMapper.mapPollToPollResponse(poll, choiceVotesMap,
creator, userVote != null ? userVote.getChoice().getId(): null);
}
    public PollResponse castVoteAndGetUpdatedPoll(Long pollId, VoteRequest voteRequest, UserPrincipal currentUser) {
Poll poll = pollRepository.findById(pollId)
.orElseThrow(() -> new ResourceNotFoundException("Poll", "id", pollId));
        if(poll.getExpirationDateTime().isBefore(Instant.now())) {
throw new BadRequestException("Sorry! This Poll has already expired");
}
        User user = userRepository.getOne(currentUser.getId());
        Choice selectedChoice = poll.getChoices().stream()
.filter(choice -> choice.getId().equals(voteRequest.getChoiceId()))
.findFirst()
.orElseThrow(() -> new ResourceNotFoundException("Choice", "id", voteRequest.getChoiceId()));
        Vote vote = new Vote();
vote.setPoll(poll);
vote.setUser(user);
vote.setChoice(selectedChoice);
        try {
vote = voteRepository.save(vote);
} catch (DataIntegrityViolationException ex) {
logger.info("User {} has already voted in Poll {}", currentUser.getId(), pollId);
throw new BadRequestException("Sorry! You have already cast your vote in this poll");
}
        //-- Vote Saved, Return the updated Poll Response now --
        // Retrieve Vote Counts of every choice belonging to the current poll
List<ChoiceVoteCount> votes = voteRepository.countByPollIdGroupByChoiceId(pollId);
        Map<Long, Long> choiceVotesMap = votes.stream()
.collect(Collectors.toMap(ChoiceVoteCount::getChoiceId, ChoiceVoteCount::getVoteCount));
        // Retrieve poll creator details
User creator = userRepository.findById(poll.getCreatedBy())
.orElseThrow(() -> new ResourceNotFoundException("User", "id", poll.getCreatedBy()));
        return ModelMapper.mapPollToPollResponse(poll, choiceVotesMap, creator, vote.getChoice().getId());
}

    private void validatePageNumberAndSize(int page, int size) {
if(page < 0) {
throw new BadRequestException("Page number cannot be less than zero.");
}
        if(size > AppConstants.MAX_PAGE_SIZE) {
throw new BadRequestException("Page size must not be greater than " + AppConstants.MAX_PAGE_SIZE);
}
}
    private Map<Long, Long> getChoiceVoteCountMap(List<Long> pollIds) {
// Retrieve Vote Counts of every Choice belonging to the given pollIds
List<ChoiceVoteCount> votes = voteRepository.countByPollIdInGroupByChoiceId(pollIds);
        Map<Long, Long> choiceVotesMap = votes.stream()
.collect(Collectors.toMap(ChoiceVoteCount::getChoiceId, ChoiceVoteCount::getVoteCount));
        return choiceVotesMap;
}
    private Map<Long, Long> getPollUserVoteMap(UserPrincipal currentUser, List<Long> pollIds) {
// Retrieve Votes done by the logged in user to the given pollIds
Map<Long, Long> pollUserVoteMap = null;
if(currentUser != null) {
List<Vote> userVotes = voteRepository.findByUserIdAndPollIdIn(currentUser.getId(), pollIds);
            pollUserVoteMap = userVotes.stream()
.collect(Collectors.toMap(vote -> vote.getPoll().getId(), vote -> vote.getChoice().getId()));
}
return pollUserVoteMap;
}
    Map<Long, User> getPollCreatorMap(List<Poll> polls) {
// Get Poll Creator details of the given list of polls
List<Long> creatorIds = polls.stream()
.map(Poll::getCreatedBy)
.distinct()
.collect(Collectors.toList());
        List<User> creators = userRepository.findByIdIn(creatorIds);
Map<Long, User> creatorMap = creators.stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
        return creatorMap;
}
}

Running the Application

You can run the application by typing the following command.

mvn spring-boot:run

Go ahead and test the APIs that we built in this article by making requests from any rest client like Postman. Write to me in the discussion section if you run into an issue.

we created the backend project, setup spring security with JWT authentication, and written the rest APIs for Login, Signup, Polls, and Users.

Then, We’ll build the client side of the application using React and Ant Design.

Bootstrapping the Front End Project

Follow the steps below to setup the front-end project.

1. Installing create-react-app

First of all, we’ll install create-react-app, a command line tool for creating react apps

npm install -g create-react-app

2. Creating the app

Now let’s create the app using create-react-app tool by typing the following command -

create-react-app polling-app-client

3. Installing Additional Dependencies

We’ll be using the following additional dependencies in our project -

  1. And Design: An excellent react based user interface library for designing the user interface.
  2. React Router: Client side routing solution for react apps.

Let’s install these dependencies by typing the following command

cd polling-app-client
npm install antd react-router-dom --save

We’ll also need some dev dependencies to customize Ant Design’s theme and enable on-demand component import. Type the following command to install these dev dependencies -

npm install react-app-rewired babel-plugin-import react-app-rewire-less --save-dev

4. Configuring Ant Design

Now let’s configure ant design and customize its theme by overriding less variables.

  • Using react-app-rewired to customize default webpack config
  • We’ll use react-app-rewired to enable customization. Open package.json file and replace the following scripts
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}

with these scripts -

"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test --env=jsdom",
"eject": "react-scripts eject"
}

Overriding configurations with config-overrides.js

Now create a file named config-overrides.js in the root directory of the project and add the following code to it -

const { injectBabelPlugin } = require('react-app-rewired');
const rewireLess = require('react-app-rewire-less');
module.exports = function override(config, env) {
config = injectBabelPlugin(['import', { libraryName: 'antd', style: true }], config);
config = rewireLess.withLoaderOptions({
modifyVars: {
"@layout-body-background": "#FFFFFF",
"@layout-header-background": "#FFFFFF",
"@layout-footer-background": "#FFFFFF"
},
javascriptEnabled: true
})(config, env);
return config;
};
  • Notice how we’re overriding Ant Design’s default less variables to customize the theme as per our needs.

5. Running the App

We’re done with all the configurations. Let’s run the app by typing the following command -

npm start

Exploring the directory structure of the Project

Following is the directory structure of the complete front-end project.

polling-app-client
↳ public
↳ favicon.png
↳ index.html
↳ manifest.json
↳ src
↳ app
↳ App.css
↳ App.js
↳ common
↳ AppHeader.css
↳ AppHeader.js
↳ LoadingIndicator.js
↳ NotFound.css
↳ NotFound.js
↳ PrivateRoute.js
↳ ServerError.css
↳ ServerError.js
↳ constants
↳ index.js
↳ poll
↳ NewPoll.css
↳ NewPoll.js
↳ Poll.css
↳ Poll.js
↳ PollList.css
↳ PollList.js
↳ user
↳ login
↳ Login.css
↳ Login.js
↳ profile
↳ Profile.css
↳ Profile.js
↳ signup
↳ Signup.css
↳ Signup.js
↳ util
↳ APIUtils.js
↳ Colors.js
↳ Helpers.js
↳ index.css
↳ index.js
↳ logo.svg
↳ poll.svg
↳ registerServiceWorker.js
↳ config-overrides.js
↳ package.json

I’ll give you a brief idea of how the project is structured and what each directory and files do.

Going through every piece of code in this blog will be very time consuming, and is absolutely unnecessary. The code is simple and self-explanatory. A basic knowledge of React is needed to understand the code.

All right! Let’s understand some of the important pieces of code in our front-end project.

Understanding the front-end code

index.js

This file is the main entry point of our react application -

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './app/App';
import registerServiceWorker from './registerServiceWorker';
import { BrowserRouter as Router } from 'react-router-dom';
ReactDOM.render(
<Router>
<App />
</Router>,
document.getElementById('root')
);
registerServiceWorker();

In the above script, we simply render the App component in a DOM element with id root (This DOM element is available in public/index.html file).

src/app/App.js

It defines the App component. The App component is the main top-level component of our application. It defines the primary layout and routing, loads the currently logged in user, and passes the currentUser and isAuthenticated property to other components.

import React, { Component } from 'react';
import './App.css';
import {
Route,
withRouter,
Switch
} from 'react-router-dom';

import { getCurrentUser } from '../util/APIUtils';
import { ACCESS_TOKEN } from '../constants';

import PollList from '../poll/PollList';
import NewPoll from '../poll/NewPoll';
import Login from '../user/login/Login';
import Signup from '../user/signup/Signup';
import Profile from '../user/profile/Profile';
import AppHeader from '../common/AppHeader';
import NotFound from '../common/NotFound';
import LoadingIndicator from '../common/LoadingIndicator';
import PrivateRoute from '../common/PrivateRoute';

import { Layout, notification } from 'antd';
const { Content } = Layout;

class App extends Component {
constructor(props) {
super(props);
this.state = {
currentUser: null,
isAuthenticated: false,
isLoading: false
}
this.handleLogout = this.handleLogout.bind(this);
this.loadCurrentUser = this.loadCurrentUser.bind(this);
this.handleLogin = this.handleLogin.bind(this);

notification.config({
placement: 'topRight',
top: 70,
duration: 3,
});
}

loadCurrentUser() {
this.setState({
isLoading: true
});
getCurrentUser()
.then(response => {
this.setState({
currentUser: response,
isAuthenticated: true,
isLoading: false
});
}).catch(error => {
this.setState({
isLoading: false
});
});
}

componentDidMount() {
this.loadCurrentUser();
}

// Handle Logout, Set currentUser and isAuthenticated state which will be passed to other components
handleLogout(redirectTo="/", notificationType="success", description="You're successfully logged out.") {
localStorage.removeItem(ACCESS_TOKEN);

this.setState({
currentUser: null,
isAuthenticated: false
});

this.props.history.push(redirectTo);

notification[notificationType]({
message: 'Polling App',
description: description,
});
}

/*
This method is called by the Login component after successful login
so that we can load the logged-in user details and set the currentUser &
isAuthenticated state, which other components will use to render their JSX
*/
handleLogin() {
notification.success({
message: 'Polling App',
description: "You're successfully logged in.",
});
this.loadCurrentUser();
this.props.history.push("/");
}

render() {
if(this.state.isLoading) {
return <LoadingIndicator />
}
return (
<Layout className="app-container">
<AppHeader isAuthenticated={this.state.isAuthenticated}
currentUser={this.state.currentUser}
onLogout={this.handleLogout} />

<Content className="app-content">
<div className="container">
<Switch>
<Route exact path="/"
render={(props) => <PollList isAuthenticated={this.state.isAuthenticated}
currentUser={this.state.currentUser} handleLogout={this.handleLogout} {...props} />}>
</Route>
<Route path="/login"
render={(props) => <Login onLogin={this.handleLogin} {...props} />}></Route>
<Route path="/signup" component={Signup}></Route>
<Route path="/users/:username"
render={(props) => <Profile isAuthenticated={this.state.isAuthenticated} currentUser={this.state.currentUser} {...props} />}>
</Route>
<PrivateRoute authenticated={this.state.isAuthenticated} path="/poll/new" component={NewPoll} handleLogout={this.handleLogout}></PrivateRoute>
<Route component={NotFound}></Route>
</Switch>
</div>
</Content>
</Layout>
);
}
}

export default withRouter(App);

src/common — Common Components

  • AppHeader.js: Header component which renders Login & SignUp buttons for unauthenticated users, and Home, Profile & Create Poll buttons for authenticated users.
  • LoadingIndicator.js: It is used by other components to render a loading indicator while an API call is in progress.
  • NotFound.js: We use this in App component to render a 404 Not Found page if none of the routes match the current url.
  • PrivateRoute.js: A meta component that redirects to /login if the user is trying to access a protected page without authentication.
  • ServerError.js: Other components use this to render a 500 Server Error page if any API responds with a 500 error which the component can’t handle.

src/constants

I’ve defined all the global constants in src/constants/index.js file for other components use -

export const API_BASE_URL = 'http://localhost:5000';
export const ACCESS_TOKEN = 'accessToken';
export const POLL_LIST_SIZE = 30;
export const MAX_CHOICES = 6;
export const POLL_QUESTION_MAX_LENGTH = 140;
export const POLL_CHOICE_MAX_LENGTH = 40;
export const NAME_MIN_LENGTH = 4;
export const NAME_MAX_LENGTH = 40;
export const USERNAME_MIN_LENGTH = 3;
export const USERNAME_MAX_LENGTH = 15;
export const EMAIL_MAX_LENGTH = 40;
export const PASSWORD_MIN_LENGTH = 6;
export const PASSWORD_MAX_LENGTH = 20;

src/poll

  • NewPoll.js: Renders the Poll creation form.
  • PollList.js: This component is used to render a list of polls. It is used to render all polls on the home page. We also use this in user’s profile page to render the list of polls created by that user, and the list of polls in which that user has voted.
  • Poll.js: It is used by the PollList component to render a single Poll.

src/user

  • login/Login.js: The Login component renders the Login form calls the login API to authenticate a user.
  • signup/Signup.js: It renders the registration form and contains a bunch of client-side validations. It’s an interesting component to check out if you want to learn how to do form validations in React.
  • profile/Profile.js: The profile page renders a user’s public profile. It displays user’s basic information, the list of polls that the user has created, and the list of polls in which the user has voted.

src/util

  • APIUtils.js: All the Rest API calls are written in this script. It uses the fetch API to make requests to the backend server.
  • Colors.js: This util is used to get a random color to use in user’s avatar.
  • Helpers.js: It contains helper functions to format dates.

Running the app

You can run the react app using npm like this -

cd polling-app-client
npm install && npm start

The above command will install any missing dependencies and start the app on port 3000.

If you just want to build the app, then type the following command -

npm run build

The above command will create an optimized production build in a directory named build/.

Over to you

I always prefer writing practical end-to-end tutorials on this blog. Full stack end-to-end tutorials like this one give you a broader picture of the application and lets you think through the challenges that are faced in all the stacks.

I hope that I didn’t overwhelm you with lots of code, and you were able to follow along and learn the concepts.

If something is unclear and you want me to explain it in detail, then let me know in the discussion section below. I’ll do my best to explain it to you.

At last, I have some homework for you :) There are still a lot of stuff that we can improve in our polls app. I want you to work on them and submit a pull request on Github.

So here are some of the improvements that will make the polls app more awesome -

  • Email verification: There is no email verification right now in our application. Anyone can register with a random email. Write the logic to send an email verification mail to newly registered users with a verification link and verify user’s email once he clicks on the link.
  • Edit Profile: Add an edit profile page where a user can change his name, username, email, and password.
  • Forgot Password: Add forgot password functionality in the app.

I encourage you to implement the above functionalities. You will find many articles on the internet explaining how to implement things like email verification, forgot password etc. If you don’t find a proper answer on the internet, then write to me in the discussion section below or send me an email. I’ll help you out.