Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
09f513a
added jjwt dependencies in pom.xml file
Semilore317 Feb 8, 2026
09c195d
chore: ignore secrets directory
Semilore317 Feb 8, 2026
b474e6b
added user entity and role-based permissions
Semilore317 Feb 8, 2026
2c0ae8b
added relationship between user and transactions
Semilore317 Feb 8, 2026
8bb695b
implemented userRepository
Semilore317 Feb 8, 2026
7804201
added database migration xml files
Semilore317 Feb 8, 2026
b29f479
created SignupRequest
Semilore317 Feb 8, 2026
5fae29c
created LoginRequest DTO
Semilore317 Feb 8, 2026
b822622
corrected LoginRequest DTO design
Semilore317 Feb 8, 2026
77ea0bd
added AuthResponse DTO
Semilore317 Feb 8, 2026
08b40b8
reformatted UserDto as a record and corrected error in User entity
Semilore317 Feb 8, 2026
990c75c
fixed inconsitencies in DTOs
Semilore317 Feb 8, 2026
636fbbd
fixed inconsistencies in DTOs
Semilore317 Feb 8, 2026
348dae9
Successfully implemented all security components with proper formatti…
Semilore317 Feb 8, 2026
17399b8
added extra exception classes
Semilore317 Feb 8, 2026
bdf4c5d
updated the GlobalExceptionHandler class
Semilore317 Feb 8, 2026
de43f43
added the EmailAlreadyExistsException class
Semilore317 Feb 8, 2026
4cd934f
added the UsernameAlreadyExistsException class
Semilore317 Feb 8, 2026
dcb7800
added the InvalidCredentialException class
Semilore317 Feb 8, 2026
4131830
modified the GlobalExceptionHandler
Semilore317 Feb 8, 2026
cfd0087
scaffolded UserService class
Semilore317 Feb 8, 2026
8cb0b43
implemented the userService
Semilore317 Feb 8, 2026
e9eb73c
removed unnecessary comments and implemented AuthController
Semilore317 Feb 8, 2026
28a163f
implemented auth controller and removed redudant comments
Semilore317 Feb 8, 2026
57a3a34
fixed bug with app not starting
Semilore317 Feb 8, 2026
104c1c1
fixed issue where JPA wasn't adding timestamps to base entitiy
Semilore317 Feb 9, 2026
175c129
modified Filter Chain to change endpoint access
Semilore317 Feb 9, 2026
850b6b1
added loggin to JwtAuthenticationFilter to detect the issue
Semilore317 Feb 9, 2026
7dc8f5c
refactored secrets file
Semilore317 Feb 9, 2026
a4d1350
fixed issue with system_admin credentials
Semilore317 Feb 9, 2026
42afa9a
removed unneeded config in application.yaml
Semilore317 Feb 10, 2026
190d21f
added preAuthorize annotation to /me endpoint
Semilore317 Feb 10, 2026
074a6fa
added JWT_SECRET to IDEA environment variables
Semilore317 Feb 10, 2026
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ build/

### VS Code ###
.vscode/

### Secrets
secrets
27 changes: 27 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,33 @@
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

<!-- Authentication & Authorization-->
<!-- Source: https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.13.0</version>
<scope>compile</scope>
</dependency>

<!-- Source: https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>

<!-- Source: https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>


</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
public class ExpenseTrackerBackendJavaApplication {

public static void main(String[] args) {
Expand Down
66 changes: 65 additions & 1 deletion src/main/java/com/SkillsForge/expensetracker/app/Config.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,67 @@
package com.SkillsForge.expensetracker.app;

public class Config {}
import com.SkillsForge.expensetracker.security.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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;
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.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity // Enables @PreAuthorize, @PostAuthorize annotations
@RequiredArgsConstructor
public class Config {

private final JwtAuthenticationFilter jwtAuthenticationFilter;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// Disable CSRF (Cross-Site Request Forgery) protection
// We use stateless JWT, so CSRF is not needed
.csrf(AbstractHttpConfigurer::disable)

// Configure endpoint authorization
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/api/v1/auth/signup",
"/api/v1/auth/login",
"/api/v1/auth/refresh-token" // if you have one
).permitAll()

// LOCKED DOORS (Everything else, including /auth/current-user)
.anyRequest().authenticated()
)
// Configure session management
// STATELESS = no server-side sessions, use JWT only
.sessionManagement(
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

// Add our JWT filter before Spring Security's authentication filter
// This ensures JWT validation happens before standard authentication
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

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

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.SkillsForge.expensetracker.app.enums;

public enum Permissions {
TRANSACTION_READ,
TRANSACTION_WRITE,
TRANSACTION_DELETE,
USER_READ,
USER_WRITE,
ADMIN_ACCESS
}
20 changes: 20 additions & 0 deletions src/main/java/com/SkillsForge/expensetracker/app/enums/Role.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.SkillsForge.expensetracker.app.enums;

import java.util.Set;

public enum Role {
USER,
ADMIN;

public Set<Permissions> getPermissions() {
return switch (this) {
case ADMIN -> Set.of(Permissions.values()); // Admin has all permissions
case USER -> Set.of(
Permissions.TRANSACTION_READ,
Permissions.TRANSACTION_WRITE,
Permissions.TRANSACTION_DELETE,
Permissions.USER_READ,
Permissions.USER_WRITE);
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.SkillsForge.expensetracker.controller;

import com.SkillsForge.expensetracker.dto.AuthResponse;
import com.SkillsForge.expensetracker.dto.LoginRequest;
import com.SkillsForge.expensetracker.dto.SignupRequest;
import com.SkillsForge.expensetracker.dto.UserDto;
import com.SkillsForge.expensetracker.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
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;

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

private final UserService userService;

@PostMapping("/signup")
public ResponseEntity<AuthResponse> signup(@Valid @RequestBody SignupRequest request) {
AuthResponse response = userService.signup(request);
return new ResponseEntity<>(response, HttpStatus.CREATED);
}

@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
AuthResponse response = userService.login(request);
return ResponseEntity.ok(response);
}


@GetMapping("/me")
Comment thread
Semilore317 marked this conversation as resolved.
@PreAuthorize("hasAuthority('USER_READ')")
public ResponseEntity<UserDto> getCurrentUser() {
UserDto user = userService.getCurrentUser();
return ResponseEntity.ok(user);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

Expand All @@ -20,23 +21,27 @@ public class TransactionController {

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@PreAuthorize("hasAuthority('TRANSACTION_WRITE')")
public TransactionDto createTransaction(
@RequestBody @Validated CreateTransactionRequest request) {
return transactionService.createTransaction(request);
}

@GetMapping("/{id}")
@PreAuthorize("hasAuthority('TRANSACTION_READ')")
public TransactionDto getTransactionById(@PathVariable Long id) {
return transactionService.getTransactionById(id);
}

@PutMapping("/{id}")
@PreAuthorize("hasAuthority('TRANSACTION_WRITE')")
public TransactionDto updateTransaction(
@PathVariable Long id, @RequestBody TransactionUpdateRequest request) {
return transactionService.updateTransaction(id, request);
}

@GetMapping
@PreAuthorize("hasAuthority('TRANSACTION_READ')")
public Page<TransactionDto> getAllTransactions(
@ModelAttribute TransactionFilter filter, Pageable pageable) {
return transactionService.getAllTransactions(filter, pageable);
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/com/SkillsForge/expensetracker/dto/AuthResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.SkillsForge.expensetracker.dto;

import com.SkillsForge.expensetracker.app.enums.Role;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthResponse {
// no need for validation, this is output
private String token; // the JWT token string
private String type; // token type, always "Bearer"
private Long id;
private String username;
private String email;
private Role role; // USER or ADMIN
}
19 changes: 19 additions & 0 deletions src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.SkillsForge.expensetracker.dto;

import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class LoginRequest {
@NotBlank(message = "Username cannot be blank")
private String username;

@NotBlank(message = "Password cannot be blank")
private String password;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.SkillsForge.expensetracker.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SignupRequest {
@NotBlank(message = "Email is required")
@Email
private String email;

@NotBlank(message = "Username cannot be blank")
@Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters")
private String username;

@NotBlank(message = "Password is required")
@Size(min = 6, max = 50, message = "Password must be between 6 and 50 characters")
private String password;
}
25 changes: 25 additions & 0 deletions src/main/java/com/SkillsForge/expensetracker/dto/UserDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.SkillsForge.expensetracker.dto;

import com.SkillsForge.expensetracker.app.enums.Role;
import com.SkillsForge.expensetracker.persistence.entity.User;
import java.time.LocalDateTime;

public record UserDto(
Long id,
String username,
String email,
Role role,
boolean enabled,
LocalDateTime createdAt,
LocalDateTime updatedAt) {
public static UserDto from(User user) {
return new UserDto(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getRole(),
user.isEnabled(),
user.getCreatedAt(),
user.getUpdatedAt());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.SkillsForge.expensetracker.exception;

public class EmailAlreadyExistsException extends RuntimeException {
public EmailAlreadyExistsException(String message) {
super(message);
}
}
Loading