Full Stack Polling Application with Spring Boot & Spring Security & JWT & MySQL & React
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 -
- Open http://start.spring.io
- Enter polls in Artifact field.
- Add Web, JPA, MySQL and Security dependencies from the Dependencies section.
- 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 -
id
: Primary Keyusername
: A unique usernameemail
: A unique emailpassword
: A password which will be stored in encrypted format.roles
: A set of roles. (Many-To-Many relationship withRole
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)
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() {}
- prePostEnabled: It enables more complex expression based access control syntax with
@PreAuthorize
and@PostAuthorize
annotations -
@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 theSecurityContext
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 -
- Many of the queries cannot be constructed by Spring-Data-Jpa’s dynamic query methods.
- Even if they can be constructed, they don’t generate an optimized query.
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 -
- And Design: An excellent react based user interface library for designing the user interface.
- 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.