Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.satwik.splitora.configuration.security.LoggedInUser;
import com.satwik.splitora.constants.SecurityConstants;
import com.satwik.splitora.constants.enums.ErrorCode;
import com.satwik.splitora.constants.enums.UserRole;
import com.satwik.splitora.persistence.dto.ErrorDetails;
import com.satwik.splitora.persistence.dto.ErrorResponseModel;
import com.satwik.splitora.repository.UserRepository;
Expand All @@ -19,6 +20,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
Expand All @@ -28,6 +30,9 @@

import java.io.IOException;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.UUID;

@Slf4j
@Component
Expand Down Expand Up @@ -76,6 +81,10 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse

// get the user email using the token
String userEmail = jwtUtil.getUserEmail(token);
// get the user id using the token
UUID userId = UUID.fromString(jwtUtil.getUserId(token));
// get the user role using the token
String userRole = jwtUtil.getUserRole(token);

// username should not be empty, cont-auth must be empty
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
Expand All @@ -87,13 +96,16 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
boolean isValid = jwtUtil.validateToken(token, userDetails);

if (isValid) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userEmail, null, userDetails.getAuthorities());

UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userEmail, null, Collections.singletonList(new SimpleGrantedAuthority(userRole)));

authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authToken);

loggedInUser.setUserEmail(userEmail);
loggedInUser.setUserId(userId);
loggedInUser.setRole(UserRole.fromString(userRole));
}
}
} catch (Exception e) {
Expand Down
50 changes: 32 additions & 18 deletions src/main/java/com/satwik/splitora/configuration/jwt/JwtUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import com.satwik.splitora.exception.RefreshTokenInvalidException;
import com.satwik.splitora.persistence.entities.User;
import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
Expand All @@ -14,10 +13,10 @@
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Component
public class JwtUtil {

private static final Logger log = LoggerFactory.getLogger(JwtUtil.class);
// secret key for access token
@Value("${jwt.access.secretKey}")
private String ACCESS_SECRET_KEY;
Expand All @@ -33,33 +32,44 @@ public class JwtUtil {

// generate access token method
public String generateAccessToken(User user) {
Map <String, Object> extraClaims = new HashMap<>();
extraClaims.put("role", "REGULAR_USER");
return buildToken(user, extraClaims, ACCESS_SECRET_KEY, ACCESS_TOKEN_EXP_TIME);
return buildToken(user, ACCESS_SECRET_KEY, ACCESS_TOKEN_EXP_TIME);
}

public String buildToken(User user, Map<String, Object> extraClaims, String secretKey, Long expirationTime) {
Date issuedAt = new Date(System.currentTimeMillis());
extraClaims.put("userId", user.getId());
return Jwts.builder()
.setSubject(user.getEmail())
.setIssuedAt(issuedAt)
.addClaims(extraClaims)
.setIssuer("com.splitora.app")
.setExpiration(new Date((expirationTime * 60 * 1000) + issuedAt.getTime()))
.signWith(SignatureAlgorithm.HS512, secretKey)
.compact();
public String buildToken(User user, String secretKey, Long expirationTime) {
try {
Date issuedAt = new Date(System.currentTimeMillis());
Map<String, Object> extraClaims = new HashMap<>();
extraClaims.put("userId", user.getId());
extraClaims.put("role", user.getUserRole());
return Jwts.builder()
.setSubject(user.getEmail())
.setIssuedAt(issuedAt)
.addClaims(extraClaims)
.setIssuer("com.splitora.app")
.setExpiration(new Date((expirationTime * 60 * 1000) + issuedAt.getTime()))
.signWith(SignatureAlgorithm.HS512, secretKey)
.compact();
} catch (Exception e) {
// Log the error message
log.info("Error while generating token: {}", e.getMessage());
throw new RuntimeException("Error while generating token: " + e.getMessage());
}
}

// generate refresh token method
public String generateRefreshToken(User user) {
return buildToken(user, new HashMap<>(), REFRESH_SECRET_KEY, REFRESH_TOKEN_EXP_TIME);
return buildToken(user, REFRESH_SECRET_KEY, REFRESH_TOKEN_EXP_TIME);
}

// get claims
private Claims getClaims(String token, String secretKey) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
}

public String getUserId(String token) {
return getClaims(token, ACCESS_SECRET_KEY).get("userId").toString();
}

public Claims getClaimsOfAccessToken(String accessToken) {
try {
return getClaims(accessToken, ACCESS_SECRET_KEY);
Expand Down Expand Up @@ -102,4 +112,8 @@ public boolean validateToken(String token, UserDetails userDetails) {
String email = getUserEmail(token);
return email != null && email.equals(userDetails.getUsername()) && !isTokenExp(token);
}

public String getUserRole(String token) {
return getClaimsOfAccessToken(token).get("role").toString();
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
package com.satwik.splitora.configuration.security;

import com.satwik.splitora.constants.enums.UserRole;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.UUID;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
public class LoggedInUser {

private String userEmail;
private String userId;
private UUID userId;
private UserRole role;

public boolean hasRole(UserRole role) {
return this.role.equals(role);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.configurers.AbstractHttpConfigurer;
Expand All @@ -21,6 +22,7 @@

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {

@Autowired
Expand Down Expand Up @@ -54,6 +56,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers(HttpMethod.GET, "/api/v1/health/ping").permitAll()
.requestMatchers(HttpMethod.GET, "/v3/api-docs/**").permitAll()
.requestMatchers(HttpMethod.GET, "/swagger-ui/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/admin**").hasAuthority("ADMIN")
.anyRequest().authenticated()
).addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class);

Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/satwik/splitora/constants/enums/UserRole.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.satwik.splitora.constants.enums;

public enum UserRole {
USER,
ADMIN,
TESTER;

public static UserRole fromString(String role) {
try {
return UserRole.valueOf(role.toUpperCase());
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid role: " + role);
}
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/satwik/splitora/controller/AdminController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.satwik.splitora.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/admin")
public class AdminController {

// TODO : work is needed for admin

@GetMapping
public ResponseEntity<String> getAdminPage() {
return ResponseEntity.ok("Welcome to the admin page!");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.nio.file.AccessDeniedException;

@Slf4j
@RestControllerAdvice
public class ApiExceptionHandler {
Expand Down Expand Up @@ -60,7 +59,7 @@ public ResponseEntity<ErrorResponseModel> handleFailedToSaveException(FailedToSa
}

@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponseModel> handleAccessDeniedException(FailedToSaveException ex) {
public ResponseEntity<ErrorResponseModel> handleAccessDeniedException(AccessDeniedException ex) {
log.info("AccessDeniedException occurred: ", ex);
ErrorResponseModel errorResponse = ResponseUtil.error("Access Denied", HttpStatus.FORBIDDEN, new ErrorDetails(
ErrorCode.ACCESS_DENIED.getCode(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.satwik.splitora.persistence.entities;

import com.satwik.splitora.constants.enums.RegistrationMethod;
import com.satwik.splitora.constants.enums.UserRole;
import jakarta.persistence.*;
import lombok.*;

Expand All @@ -12,7 +13,9 @@
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "user")
@Table(name = "user", uniqueConstraints = {
@UniqueConstraint(columnNames = {"phone_country_code", "phone_number"})
})
public class User extends BaseEntity {

@Column(name = "username", unique = true)
Expand All @@ -34,6 +37,10 @@ public class User extends BaseEntity {
@Column(name = "registrationMethod")
private RegistrationMethod registrationMethod;

@Enumerated(EnumType.STRING)
@Column(name = "user_role", nullable = false, length = 20)
private UserRole userRole;

@OneToMany(mappedBy = "payer", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<Expense> expenseList;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
package com.satwik.splitora.service.implementations;

import com.satwik.splitora.configuration.security.LoggedInUser;
import com.satwik.splitora.constants.enums.UserRole;
import com.satwik.splitora.exception.DataNotFoundException;
import com.satwik.splitora.persistence.entities.Expense;
import com.satwik.splitora.persistence.entities.Group;
import com.satwik.splitora.persistence.entities.User;
import com.satwik.splitora.repository.ExpenseRepository;
import com.satwik.splitora.repository.GroupRepository;
import com.satwik.splitora.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Component;

@Service
import java.util.UUID;

@Component("authorizationService")
public class AuthorizationService {

@Autowired
Expand All @@ -16,8 +23,33 @@ public class AuthorizationService {
@Autowired
UserRepository userRepository;

@Autowired
GroupRepository groupRepository;

@Autowired
ExpenseRepository expenseRepository;


public User getAuthorizedUser() {
return userRepository.findByEmail(loggedInUser.getUserEmail()).orElseThrow(() -> new DataNotFoundException("User not found"));
}

public boolean isGroupOwner(UUID groupId) {
if (loggedInUser.hasRole(UserRole.ADMIN))
return true;

Group group = groupRepository.findById(groupId).orElseThrow(() -> new DataNotFoundException("Group not found"));
UUID ownerId = group.getUser().getId();
return loggedInUser.getUserId().equals(ownerId);
}

public boolean isExpenseOwner(UUID expenseId) {
if (loggedInUser.hasRole(UserRole.ADMIN))
return true;

Expense expense = expenseRepository.findById(expenseId).orElseThrow(() -> new DataNotFoundException("Expense not found"));
UUID ownerId = expense.getPayer().getId();
return loggedInUser.getUserId().equals(ownerId);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundExcep
return org.springframework.security.core.userdetails.User.builder()
.username(user.getEmail())
.password(user.getPassword())
.roles("USER")
.roles(user.getUserRole().toString())
.build();
}
}
Loading