Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app(backend)/.env-sample
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
DB_URL= postgresSql_database_url
DB_USERNAME= postgresSql_database_username
DB_PASSWORD= postgresSql_database_password

JWT_SECRET_KEY=your_secret_key
JWT_EXP_TIME=expiration_time
JWT_THRESHOLD_TIME=threshold_time

REDIS_HOST=redis_host
REDIS_PORT=redis_port
21 changes: 21 additions & 0 deletions app(backend)/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,27 @@
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.knockoutzone.backend.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class AppConfig {

@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.knockoutzone.backend.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(connectionFactory);
return template;
}

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
return template;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.knockoutzone.backend.config;

import com.knockoutzone.backend.filters.JwtAuthenticationFilter;
import com.knockoutzone.backend.serviceimpl.TokenBlackListService;
import com.knockoutzone.backend.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig {

private final JwtUtil jwtUtil;
private final TokenBlackListService tokenBlackListService;

@Autowired
public SecurityConfig(JwtUtil jwtUtil, TokenBlackListService tokenBlackListService) {
this.jwtUtil = jwtUtil;
this.tokenBlackListService = tokenBlackListService;
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

JwtAuthenticationFilter jwtAuthFilter = new JwtAuthenticationFilter(jwtUtil, tokenBlackListService);

http
.csrf(csrf -> csrf.disable()) // Disable CSRF for REST APIs
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/register", "/api/auth/login").permitAll() // Allow public access to register and login endpoint
.anyRequest().authenticated() // Secure all other endpoints
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.knockoutzone.backend.controller;

import com.knockoutzone.backend.dto.*;
import com.knockoutzone.backend.service.AuthService;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

@Autowired
private AuthService authService;

public AuthController(AuthService authService) {
this.authService = authService;
}

@PostMapping("/register")
public ResponseEntity<UserRegistrationResponse> register(@RequestBody UserRegistrationRequest request){
UserRegistrationResponse response = authService.registerUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody UserLoginRequest request) {
String jwtToken = authService.authenticateUser(request);
return ResponseEntity.ok().header(HttpHeaders.AUTHORIZATION, "Bearer " + jwtToken).body(Constants.USER_LOGIN_SUCCESSFUL);
}

@PostMapping("/logout")
public ResponseEntity<?> logout(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");

if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return ResponseEntity.badRequest().body("Missing or invalid Authorization header");
}

return ResponseEntity.ok(authService.logoutUser(authHeader));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.knockoutzone.backend.dto;

public class Constants {

public static final String INVALID_EMAIL = "Invalid email format.";
public static final String EMAIL_ALREADY_USED = "Email already in use.";
public static final String WEAK_PASSWORD = "Password is too weak. Must be 8+ chars, with uppercase, lowercase, digit and special character.";
public static final String FULL_NAME_REQUIRED = "Full name is required.";
public static final String REGISTRATION_SUCCESS = "User registered successfully!";
public static final String USER_NOT_FOUND = "User not found";
public static final String PASSWORD_MISMATCH = "Invalid password";
public static final String USER_LOGIN_SUCCESSFUL = "Login successful.";
public static final String USER_LOGOUT_SUCCESSFUL = "Logged out successfully.";
public static final String TOKEN_REVOKED = "Token has been revoked.";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.knockoutzone.backend.dto;

import lombok.Data;

@Data
public class UserLoginRequest {

private String email;
private String password;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.knockoutzone.backend.dto;

import com.knockoutzone.backend.entity.enums.Role;
import lombok.Data;

@Data
public class UserRegistrationRequest {

private String fullName;
private String email;
private String password;
private Role role;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.knockoutzone.backend.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class UserRegistrationResponse {
int statusCode;
String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.knockoutzone.backend.entity;

import com.knockoutzone.backend.entity.enums.Role;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import java.time.LocalDateTime;

@Entity
@Table(name = "users")
@Data
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "full_name", nullable = false)
private String fullName;

@Column(unique = true, length = 100, nullable = false)
private String email;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;

@Column(name = "password_hash", nullable = false)
private String passwordHash;

@CreationTimestamp
private LocalDateTime createdAt;

@UpdateTimestamp
private LocalDateTime updatedAt;

private boolean isDeleted = false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.knockoutzone.backend.entity.enums;

public enum Role {
PLAYER,
ADMIN,
ORGANISER
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.knockoutzone.backend.filters;

import com.knockoutzone.backend.dto.Constants;
import com.knockoutzone.backend.serviceimpl.TokenBlackListService;
import com.knockoutzone.backend.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Value("${security.jwt.threshold.time}")
private long thresholdTime;

private final JwtUtil jwtUtil;
private final TokenBlackListService tokenBlackListService;

@Autowired
public JwtAuthenticationFilter(JwtUtil jwtUtil, TokenBlackListService tokenBlackListService) {
this.jwtUtil = jwtUtil;
this.tokenBlackListService = tokenBlackListService;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

final String authHeader = request.getHeader("Authorization");

if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}

final String jwt = authHeader.substring(7);

if(jwtUtil.isTokenValid(jwt)) {

String jti = jwtUtil.extractJti(jwt);
if(tokenBlackListService.isTokenBlacklisted(jti)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write(Constants.TOKEN_REVOKED);
return;
}

String userName = jwtUtil.extractUsername(jwt);

if (userName != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userName, null, Collections.emptyList());

SecurityContextHolder.getContext().setAuthentication(authToken);
}

checkIfTokenExpired(jwt, userName, response);
}

filterChain.doFilter(request, response);
}

public void checkIfTokenExpired(String token, String userName, HttpServletResponse response){

Date expirationTime = jwtUtil.extractExpiration(token);
long timeLeft = expirationTime.getTime() - System.currentTimeMillis();

if (timeLeft < thresholdTime) { // less than 5 minutes left
// Generate new token with refreshed expiry
String newToken = jwtUtil.generateToken(userName);

// Send new token back in response header (or response body)
response.setHeader("Authorization", "Bearer " + newToken);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.knockoutzone.backend.repository;

import com.knockoutzone.backend.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
boolean existsByEmail(String email);
Optional<User> findByEmail(String email);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.knockoutzone.backend.service;

import com.knockoutzone.backend.dto.UserLoginRequest;
import com.knockoutzone.backend.dto.UserRegistrationRequest;
import com.knockoutzone.backend.dto.UserRegistrationResponse;

public interface AuthService {

UserRegistrationResponse registerUser(UserRegistrationRequest userRegistrationRequest);

String authenticateUser(UserLoginRequest userLoginRequest);

String logoutUser(String authHeader);
}
Empty file.
Loading