From 09f513a3989591ce01b72be802f8014efa015722 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 18:56:07 +0100 Subject: [PATCH 01/33] added jjwt dependencies in pom.xml file --- pom.xml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pom.xml b/pom.xml index c93f7d5..53ac478 100644 --- a/pom.xml +++ b/pom.xml @@ -77,6 +77,33 @@ spring-security-test test + + + + + io.jsonwebtoken + jjwt-api + 0.13.0 + compile + + + + + io.jsonwebtoken + jjwt-impl + 0.13.0 + runtime + + + + + io.jsonwebtoken + jjwt-jackson + 0.13.0 + runtime + + + From 09c195dc4dd49e8344902ef54f7baacec5e31109 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 19:48:05 +0100 Subject: [PATCH 02/33] chore: ignore secrets directory --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 667aaef..736bc8c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ build/ ### VS Code ### .vscode/ + +### Secrets +secrets \ No newline at end of file From b474e6b401428aa8252fc6fe497fd86ea50d2e6e Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 19:53:51 +0100 Subject: [PATCH 03/33] added user entity and role-based permissions --- .../expensetracker/app/enums/Permissions.java | 10 ++++ .../expensetracker/app/enums/Role.java | 6 +++ .../persistence/entity/User.java | 48 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 src/main/java/com/SkillsForge/expensetracker/app/enums/Permissions.java create mode 100644 src/main/java/com/SkillsForge/expensetracker/app/enums/Role.java create mode 100644 src/main/java/com/SkillsForge/expensetracker/persistence/entity/User.java diff --git a/src/main/java/com/SkillsForge/expensetracker/app/enums/Permissions.java b/src/main/java/com/SkillsForge/expensetracker/app/enums/Permissions.java new file mode 100644 index 0000000..ec7ddb2 --- /dev/null +++ b/src/main/java/com/SkillsForge/expensetracker/app/enums/Permissions.java @@ -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 +} diff --git a/src/main/java/com/SkillsForge/expensetracker/app/enums/Role.java b/src/main/java/com/SkillsForge/expensetracker/app/enums/Role.java new file mode 100644 index 0000000..35cc789 --- /dev/null +++ b/src/main/java/com/SkillsForge/expensetracker/app/enums/Role.java @@ -0,0 +1,6 @@ +package com.SkillsForge.expensetracker.app.enums; + +public enum Role { + USER, + ADMIN +} diff --git a/src/main/java/com/SkillsForge/expensetracker/persistence/entity/User.java b/src/main/java/com/SkillsForge/expensetracker/persistence/entity/User.java new file mode 100644 index 0000000..5e2fcc6 --- /dev/null +++ b/src/main/java/com/SkillsForge/expensetracker/persistence/entity/User.java @@ -0,0 +1,48 @@ +package com.SkillsForge.expensetracker.persistence.entity; + +import com.SkillsForge.expensetracker.app.enums.Role; +import jakarta.persistence.*; +import lombok.*; +import lombok.experimental.Accessors; +import lombok.experimental.SuperBuilder; + +@Accessors(chain=true) +@Entity +@Table(name = "users") +@Getter +@Setter +@ToString +@SuperBuilder +@RequiredArgsConstructor +public class User extends BaseEntity{ + + @Column(unique = true, nullable = false) + private String username; + + @Column(unique = true, nullable = false) + private String email; + + @Column(nullable = false) + private String password; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + @Builder.Default + private Role role; + + @Column(nullable = false) + @Builder.Default + private boolean enabled = true; + + @Column(nullable = false) + @Builder.Default + private boolean accountNonExpired = true; + + @Column(nullable = false) + @Builder.Default + private boolean accountNonLocked = true; + + @Column(nullable = false) + @Builder.Default + private boolean credentialsNonExpired = true; +} From 2c0ae8b635b4df1b867325deff9965982ab6b2f0 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 19:54:28 +0100 Subject: [PATCH 04/33] added relationship between user and transactions --- .../expensetracker/persistence/entity/Transaction.java | 6 ++++++ src/main/resources/application.yaml | 3 +++ 2 files changed, 9 insertions(+) diff --git a/src/main/java/com/SkillsForge/expensetracker/persistence/entity/Transaction.java b/src/main/java/com/SkillsForge/expensetracker/persistence/entity/Transaction.java index f11327a..6c7dac0 100644 --- a/src/main/java/com/SkillsForge/expensetracker/persistence/entity/Transaction.java +++ b/src/main/java/com/SkillsForge/expensetracker/persistence/entity/Transaction.java @@ -11,6 +11,7 @@ import lombok.ToString; import lombok.experimental.Accessors; import lombok.experimental.SuperBuilder; +import org.apache.logging.log4j.util.Lazy; @Accessors(chain = true) @Entity @@ -38,6 +39,11 @@ public class Transaction extends BaseEntity { @Column(nullable = false) private Long amount; // We save amount in kobo value so 1.50 naira will be saved as 150 + // user relationship + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + // constructor to accept dto public Transaction(CreateTransactionRequest request) { this.description = request.getDescription(); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index e454b0f..7ed98d0 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -21,6 +21,9 @@ spring: # default-schema: public # drop-first: false + jwt: + secret: ${JWT_SECRET:file:./secrets/jwt.secret} + expiration: ${JWT_EXPIRATION:86400000} server: port: ${SERVER_PORT:8080} \ No newline at end of file From 8bb695b9aa54719d21b4c434511a5ede934018c6 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 19:54:36 +0100 Subject: [PATCH 05/33] implemented userRepository --- .../persistence/repository/UserRepository.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/main/java/com/SkillsForge/expensetracker/persistence/repository/UserRepository.java diff --git a/src/main/java/com/SkillsForge/expensetracker/persistence/repository/UserRepository.java b/src/main/java/com/SkillsForge/expensetracker/persistence/repository/UserRepository.java new file mode 100644 index 0000000..6c7b0fc --- /dev/null +++ b/src/main/java/com/SkillsForge/expensetracker/persistence/repository/UserRepository.java @@ -0,0 +1,13 @@ +package com.SkillsForge.expensetracker.persistence.repository; + +import com.SkillsForge.expensetracker.persistence.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); + Optional findByEmail(String email); + boolean existsByUsername(String username); + boolean existsByEmail(String email); +} From 7804201240c00df906d919ae6553b961c5b853e4 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 20:09:38 +0100 Subject: [PATCH 06/33] added database migration xml files --- .../db/changelog/002_user_migration.xml | 67 +++++++++++++++ .../changelog/003_add_user_to_transaction.xml | 81 +++++++++++++++++++ .../db/changelog/changelog-master.xml | 2 + 3 files changed, 150 insertions(+) create mode 100644 src/main/resources/db/changelog/002_user_migration.xml create mode 100644 src/main/resources/db/changelog/003_add_user_to_transaction.xml diff --git a/src/main/resources/db/changelog/002_user_migration.xml b/src/main/resources/db/changelog/002_user_migration.xml new file mode 100644 index 0000000..bb3b2e9 --- /dev/null +++ b/src/main/resources/db/changelog/002_user_migration.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/db/changelog/003_add_user_to_transaction.xml b/src/main/resources/db/changelog/003_add_user_to_transaction.xml new file mode 100644 index 0000000..c219908 --- /dev/null +++ b/src/main/resources/db/changelog/003_add_user_to_transaction.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + SELECT COUNT(*) FROM users WHERE username = 'system_admin' + + + + + + + + + + + + + + + + + + + + + + + SELECT COUNT(*) FROM users WHERE username = 'system_admin' + + + + + UPDATE transactions + SET user_id = (SELECT id FROM users WHERE username = 'system_admin') + WHERE user_id IS NULL + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/db/changelog/changelog-master.xml b/src/main/resources/db/changelog/changelog-master.xml index 69815ec..015b05a 100644 --- a/src/main/resources/db/changelog/changelog-master.xml +++ b/src/main/resources/db/changelog/changelog-master.xml @@ -5,4 +5,6 @@ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> + + \ No newline at end of file From b29f4799f4958405c8cf1daf8a3c97778decc399 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 20:19:43 +0100 Subject: [PATCH 07/33] created SignupRequest --- .../expensetracker/dto/SignupRequest.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java diff --git a/src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java b/src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java new file mode 100644 index 0000000..472a3ab --- /dev/null +++ b/src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java @@ -0,0 +1,25 @@ +package com.SkillsForge.expensetracker.dto; + +import jakarta.persistence.Column; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@AllArgsConstructor +public class SignupRequest { + @NotBlank(message = "Email is required") + @Email + private String email; + + @NotBlank(message = "Username cannot be blank") + @Size(min = 3, max = 20) + private String username; + + @NotBlank(message = "Password is required") + private Long password; +} From 5fae29c2b72727b6ce9772f98dcfb1368b86ab4b Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 20:22:37 +0100 Subject: [PATCH 08/33] created LoginRequest DTO --- .../expensetracker/dto/LoginRequest.java | 21 +++++++++++++++++++ .../expensetracker/dto/SignupRequest.java | 3 ++- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java diff --git a/src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java b/src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java new file mode 100644 index 0000000..aa70341 --- /dev/null +++ b/src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java @@ -0,0 +1,21 @@ +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; + +@Data +@Builder +@AllArgsConstructor +public class LoginRequest { + @Email + @NotBlank(message = "Email cannot be blank") + private String email; + + @NotBlank + @Size(min = 6, max = 20, message = "Password must be between 6 and 20 characters") + private String password; +} diff --git a/src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java b/src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java index 472a3ab..0741e7c 100644 --- a/src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java +++ b/src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java @@ -17,9 +17,10 @@ public class SignupRequest { private String email; @NotBlank(message = "Username cannot be blank") - @Size(min = 3, max = 20) + @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 = 20, message = "Message must be between 6 and 20 characters") private Long password; } From b82262238d0b241e02791d0b13599b9004116aee Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 20:23:29 +0100 Subject: [PATCH 09/33] corrected LoginRequest DTO design --- .../java/com/SkillsForge/expensetracker/dto/LoginRequest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java b/src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java index aa70341..cad76e6 100644 --- a/src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java +++ b/src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java @@ -11,11 +11,9 @@ @Builder @AllArgsConstructor public class LoginRequest { - @Email @NotBlank(message = "Email cannot be blank") private String email; @NotBlank - @Size(min = 6, max = 20, message = "Password must be between 6 and 20 characters") private String password; } From 77ea0bdcf447acd2f4cdc5d488eb84e4c0f4ccc1 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 20:30:03 +0100 Subject: [PATCH 10/33] added AuthResponse DTO --- .../expensetracker/dto/AuthResponse.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/java/com/SkillsForge/expensetracker/dto/AuthResponse.java diff --git a/src/main/java/com/SkillsForge/expensetracker/dto/AuthResponse.java b/src/main/java/com/SkillsForge/expensetracker/dto/AuthResponse.java new file mode 100644 index 0000000..e69d4a1 --- /dev/null +++ b/src/main/java/com/SkillsForge/expensetracker/dto/AuthResponse.java @@ -0,0 +1,19 @@ +package com.SkillsForge.expensetracker.dto; + +import com.SkillsForge.expensetracker.app.enums.Role; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@AllArgsConstructor +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 +} From 08b40b8603c610f157a85c409981609133a407e3 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 20:42:44 +0100 Subject: [PATCH 11/33] reformatted UserDto as a record and corrected error in User entity --- .../expensetracker/dto/UserDto.java | 28 +++++++++++++++++++ .../persistence/entity/User.java | 5 ++-- 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/SkillsForge/expensetracker/dto/UserDto.java diff --git a/src/main/java/com/SkillsForge/expensetracker/dto/UserDto.java b/src/main/java/com/SkillsForge/expensetracker/dto/UserDto.java new file mode 100644 index 0000000..bed1b18 --- /dev/null +++ b/src/main/java/com/SkillsForge/expensetracker/dto/UserDto.java @@ -0,0 +1,28 @@ +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() + ); + } +} diff --git a/src/main/java/com/SkillsForge/expensetracker/persistence/entity/User.java b/src/main/java/com/SkillsForge/expensetracker/persistence/entity/User.java index 5e2fcc6..cc87fdb 100644 --- a/src/main/java/com/SkillsForge/expensetracker/persistence/entity/User.java +++ b/src/main/java/com/SkillsForge/expensetracker/persistence/entity/User.java @@ -13,7 +13,8 @@ @Setter @ToString @SuperBuilder -@RequiredArgsConstructor +@NoArgsConstructor +@AllArgsConstructor public class User extends BaseEntity{ @Column(unique = true, nullable = false) @@ -28,7 +29,7 @@ public class User extends BaseEntity{ @Enumerated(EnumType.STRING) @Column(nullable = false) @Builder.Default - private Role role; + private Role role = Role.USER; @Column(nullable = false) @Builder.Default From 990c75c936cec22e957accbe00c58d3d664e2efb Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 20:59:09 +0100 Subject: [PATCH 12/33] fixed inconsitencies in DTOs --- .../com/SkillsForge/expensetracker/dto/AuthResponse.java | 2 ++ .../com/SkillsForge/expensetracker/dto/LoginRequest.java | 8 ++++---- .../com/SkillsForge/expensetracker/dto/SignupRequest.java | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/SkillsForge/expensetracker/dto/AuthResponse.java b/src/main/java/com/SkillsForge/expensetracker/dto/AuthResponse.java index e69d4a1..fa56f8c 100644 --- a/src/main/java/com/SkillsForge/expensetracker/dto/AuthResponse.java +++ b/src/main/java/com/SkillsForge/expensetracker/dto/AuthResponse.java @@ -4,10 +4,12 @@ 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 diff --git a/src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java b/src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java index cad76e6..455eaf3 100644 --- a/src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java +++ b/src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java @@ -1,18 +1,18 @@ 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 LoginRequest { - @NotBlank(message = "Email cannot be blank") - private String email; + @NotBlank(message = "Username cannot be blank") + private String username; @NotBlank private String password; diff --git a/src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java b/src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java index 0741e7c..eb63ac2 100644 --- a/src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java +++ b/src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java @@ -1,16 +1,17 @@ package com.SkillsForge.expensetracker.dto; -import jakarta.persistence.Column; 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 @@ -22,5 +23,5 @@ public class SignupRequest { @NotBlank(message = "Password is required") @Size(min = 6, max = 20, message = "Message must be between 6 and 20 characters") - private Long password; + private String password; } From 636fbbd336195d6659137ced86abe3ab44cfe95a Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 21:00:28 +0100 Subject: [PATCH 13/33] fixed inconsistencies in DTOs --- .../java/com/SkillsForge/expensetracker/dto/LoginRequest.java | 2 +- .../java/com/SkillsForge/expensetracker/dto/SignupRequest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java b/src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java index 455eaf3..ec3f687 100644 --- a/src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java +++ b/src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java @@ -14,6 +14,6 @@ public class LoginRequest { @NotBlank(message = "Username cannot be blank") private String username; - @NotBlank + @NotBlank(message = "Password cannot be blank") private String password; } diff --git a/src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java b/src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java index eb63ac2..dd66f38 100644 --- a/src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java +++ b/src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java @@ -22,6 +22,6 @@ public class SignupRequest { private String username; @NotBlank(message = "Password is required") - @Size(min = 6, max = 20, message = "Message must be between 6 and 20 characters") + @Size(min = 6, max = 50, message = "Password must be between 6 and 50 characters") private String password; } From 348dae9c105a1aa84ac1dc7d603d55ba13f348c8 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 21:30:24 +0100 Subject: [PATCH 14/33] Successfully implemented all security components with proper formatting: JwtUtil, UserDetailsServiceImpl, JwtAuthenticationFilter, SecurityConfig, and added getPermissions() method to Role enum. Code formatting applied via Spotless --- .../expensetracker/app/Config.java | 91 +++++++++++- .../expensetracker/app/enums/Permissions.java | 12 +- .../expensetracker/app/enums/Role.java | 23 ++- .../expensetracker/dto/AuthResponse.java | 14 +- .../expensetracker/dto/LoginRequest.java | 8 +- .../expensetracker/dto/SignupRequest.java | 18 +-- .../expensetracker/dto/UserDto.java | 25 ++-- .../persistence/entity/Transaction.java | 9 +- .../persistence/entity/User.java | 48 +++--- .../repository/UserRepository.java | 14 +- .../security/JwtAuthenticationFilter.java | 87 +++++++++++ .../expensetracker/security/JwtUtil.java | 137 ++++++++++++++++++ .../security/UserDetailsServiceImpl.java | 85 +++++++++++ 13 files changed, 493 insertions(+), 78 deletions(-) create mode 100644 src/main/java/com/SkillsForge/expensetracker/security/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/SkillsForge/expensetracker/security/JwtUtil.java create mode 100644 src/main/java/com/SkillsForge/expensetracker/security/UserDetailsServiceImpl.java diff --git a/src/main/java/com/SkillsForge/expensetracker/app/Config.java b/src/main/java/com/SkillsForge/expensetracker/app/Config.java index 1c94cb7..a5bda1b 100644 --- a/src/main/java/com/SkillsForge/expensetracker/app/Config.java +++ b/src/main/java/com/SkillsForge/expensetracker/app/Config.java @@ -1,3 +1,92 @@ 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; + +/** + * Spring Security Configuration Configures authentication, authorization, and JWT token validation + */ +@Configuration +@EnableWebSecurity +@EnableMethodSecurity // Enables @PreAuthorize, @PostAuthorize annotations +@RequiredArgsConstructor +public class Config { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + /** + * Configure the security filter chain Defines which endpoints are public and which require + * authentication + * + * @param http HttpSecurity to configure + * @return configured SecurityFilterChain + * @throws Exception if configuration fails + */ + @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 + // Public endpoints - anyone can access + .requestMatchers("/api/auth/**") + .permitAll() + + // All other endpoints require authentication + .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(); + } + + /** + * Password encoder bean for hashing passwords BCrypt is a strong hashing algorithm recommended + * for passwords + * + * @return BCryptPasswordEncoder instance + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + /** + * Authentication manager bean Used for authenticating users during login + * + * @param config authentication configuration + * @return AuthenticationManager + * @throws Exception if configuration fails + */ + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) + throws Exception { + return config.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/SkillsForge/expensetracker/app/enums/Permissions.java b/src/main/java/com/SkillsForge/expensetracker/app/enums/Permissions.java index ec7ddb2..90e8ae8 100644 --- a/src/main/java/com/SkillsForge/expensetracker/app/enums/Permissions.java +++ b/src/main/java/com/SkillsForge/expensetracker/app/enums/Permissions.java @@ -1,10 +1,10 @@ package com.SkillsForge.expensetracker.app.enums; public enum Permissions { - TRANSACTION_READ, - TRANSACTION_WRITE, - TRANSACTION_DELETE, - USER_READ, - USER_WRITE, - ADMIN_ACCESS + TRANSACTION_READ, + TRANSACTION_WRITE, + TRANSACTION_DELETE, + USER_READ, + USER_WRITE, + ADMIN_ACCESS } diff --git a/src/main/java/com/SkillsForge/expensetracker/app/enums/Role.java b/src/main/java/com/SkillsForge/expensetracker/app/enums/Role.java index 35cc789..0280e52 100644 --- a/src/main/java/com/SkillsForge/expensetracker/app/enums/Role.java +++ b/src/main/java/com/SkillsForge/expensetracker/app/enums/Role.java @@ -1,6 +1,25 @@ package com.SkillsForge.expensetracker.app.enums; +import java.util.Set; + public enum Role { - USER, - ADMIN + USER, + ADMIN; + + /** + * Get the permissions associated with this role + * + * @return set of permissions for this role + */ + public Set 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); + }; + } } diff --git a/src/main/java/com/SkillsForge/expensetracker/dto/AuthResponse.java b/src/main/java/com/SkillsForge/expensetracker/dto/AuthResponse.java index fa56f8c..e7d2072 100644 --- a/src/main/java/com/SkillsForge/expensetracker/dto/AuthResponse.java +++ b/src/main/java/com/SkillsForge/expensetracker/dto/AuthResponse.java @@ -11,11 +11,11 @@ @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 + // 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 } diff --git a/src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java b/src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java index ec3f687..5d26f0e 100644 --- a/src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java +++ b/src/main/java/com/SkillsForge/expensetracker/dto/LoginRequest.java @@ -11,9 +11,9 @@ @AllArgsConstructor @NoArgsConstructor public class LoginRequest { - @NotBlank(message = "Username cannot be blank") - private String username; + @NotBlank(message = "Username cannot be blank") + private String username; - @NotBlank(message = "Password cannot be blank") - private String password; + @NotBlank(message = "Password cannot be blank") + private String password; } diff --git a/src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java b/src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java index dd66f38..d68976d 100644 --- a/src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java +++ b/src/main/java/com/SkillsForge/expensetracker/dto/SignupRequest.java @@ -13,15 +13,15 @@ @AllArgsConstructor @NoArgsConstructor public class SignupRequest { - @NotBlank(message = "Email is required") - @Email - private String email; + @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 = "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; + @NotBlank(message = "Password is required") + @Size(min = 6, max = 50, message = "Password must be between 6 and 50 characters") + private String password; } diff --git a/src/main/java/com/SkillsForge/expensetracker/dto/UserDto.java b/src/main/java/com/SkillsForge/expensetracker/dto/UserDto.java index bed1b18..4893732 100644 --- a/src/main/java/com/SkillsForge/expensetracker/dto/UserDto.java +++ b/src/main/java/com/SkillsForge/expensetracker/dto/UserDto.java @@ -2,7 +2,6 @@ import com.SkillsForge.expensetracker.app.enums.Role; import com.SkillsForge.expensetracker.persistence.entity.User; - import java.time.LocalDateTime; public record UserDto( @@ -12,17 +11,15 @@ public record UserDto( 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() - ); - } + 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()); + } } diff --git a/src/main/java/com/SkillsForge/expensetracker/persistence/entity/Transaction.java b/src/main/java/com/SkillsForge/expensetracker/persistence/entity/Transaction.java index 6c7dac0..95ac14e 100644 --- a/src/main/java/com/SkillsForge/expensetracker/persistence/entity/Transaction.java +++ b/src/main/java/com/SkillsForge/expensetracker/persistence/entity/Transaction.java @@ -11,7 +11,6 @@ import lombok.ToString; import lombok.experimental.Accessors; import lombok.experimental.SuperBuilder; -import org.apache.logging.log4j.util.Lazy; @Accessors(chain = true) @Entity @@ -39,10 +38,10 @@ public class Transaction extends BaseEntity { @Column(nullable = false) private Long amount; // We save amount in kobo value so 1.50 naira will be saved as 150 - // user relationship - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User user; + // user relationship + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; // constructor to accept dto public Transaction(CreateTransactionRequest request) { diff --git a/src/main/java/com/SkillsForge/expensetracker/persistence/entity/User.java b/src/main/java/com/SkillsForge/expensetracker/persistence/entity/User.java index cc87fdb..25b794d 100644 --- a/src/main/java/com/SkillsForge/expensetracker/persistence/entity/User.java +++ b/src/main/java/com/SkillsForge/expensetracker/persistence/entity/User.java @@ -6,7 +6,7 @@ import lombok.experimental.Accessors; import lombok.experimental.SuperBuilder; -@Accessors(chain=true) +@Accessors(chain = true) @Entity @Table(name = "users") @Getter @@ -15,35 +15,35 @@ @SuperBuilder @NoArgsConstructor @AllArgsConstructor -public class User extends BaseEntity{ +public class User extends BaseEntity { - @Column(unique = true, nullable = false) - private String username; + @Column(unique = true, nullable = false) + private String username; - @Column(unique = true, nullable = false) - private String email; + @Column(unique = true, nullable = false) + private String email; - @Column(nullable = false) - private String password; + @Column(nullable = false) + private String password; - @Enumerated(EnumType.STRING) - @Column(nullable = false) - @Builder.Default - private Role role = Role.USER; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + @Builder.Default + private Role role = Role.USER; - @Column(nullable = false) - @Builder.Default - private boolean enabled = true; + @Column(nullable = false) + @Builder.Default + private boolean enabled = true; - @Column(nullable = false) - @Builder.Default - private boolean accountNonExpired = true; + @Column(nullable = false) + @Builder.Default + private boolean accountNonExpired = true; - @Column(nullable = false) - @Builder.Default - private boolean accountNonLocked = true; + @Column(nullable = false) + @Builder.Default + private boolean accountNonLocked = true; - @Column(nullable = false) - @Builder.Default - private boolean credentialsNonExpired = true; + @Column(nullable = false) + @Builder.Default + private boolean credentialsNonExpired = true; } diff --git a/src/main/java/com/SkillsForge/expensetracker/persistence/repository/UserRepository.java b/src/main/java/com/SkillsForge/expensetracker/persistence/repository/UserRepository.java index 6c7b0fc..74da654 100644 --- a/src/main/java/com/SkillsForge/expensetracker/persistence/repository/UserRepository.java +++ b/src/main/java/com/SkillsForge/expensetracker/persistence/repository/UserRepository.java @@ -1,13 +1,15 @@ package com.SkillsForge.expensetracker.persistence.repository; import com.SkillsForge.expensetracker.persistence.entity.User; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); + + Optional findByEmail(String email); + + boolean existsByUsername(String username); -public interface UserRepository extends JpaRepository { - Optional findByUsername(String username); - Optional findByEmail(String email); - boolean existsByUsername(String username); - boolean existsByEmail(String email); + boolean existsByEmail(String email); } diff --git a/src/main/java/com/SkillsForge/expensetracker/security/JwtAuthenticationFilter.java b/src/main/java/com/SkillsForge/expensetracker/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..b3f900f --- /dev/null +++ b/src/main/java/com/SkillsForge/expensetracker/security/JwtAuthenticationFilter.java @@ -0,0 +1,87 @@ +package com.SkillsForge.expensetracker.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.lang.NonNull; +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.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * JWT Authentication Filter that intercepts every HTTP request. Validates JWT token and sets + * authentication in Spring Security context. + */ +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final UserDetailsServiceImpl userDetailsService; + + /** + * Filter method that runs once per request. Extracts and validates JWT token, then sets + * authentication. + * + * @param request HTTP request + * @param response HTTP response + * @param filterChain filter chain to continue processing + * @throws ServletException if servlet error occurs + * @throws IOException if I/O error occurs + */ + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) + throws ServletException, IOException { + + // 1. Extract the Authorization header from the request + final String authorizationHeader = request.getHeader("Authorization"); + + String username = null; + String jwt = null; + + // 2. Check if header exists and starts with "Bearer " + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + jwt = authorizationHeader.substring(7); // Remove "Bearer " prefix to get token + try { + username = jwtUtil.extractUsername(jwt); // Extract username from token + } catch (Exception e) { + // Token is invalid or expired - log and continue without authentication + logger.warn("JWT token extraction failed: " + e.getMessage()); + } + } + + // 3. If we have a username and no authentication exists in SecurityContext + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + + // Load user details from database + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + // Validate the token + if (jwtUtil.validateToken(jwt, userDetails)) { + // Token is valid - create authentication object + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + + // Set additional details (IP address, session ID, etc.) + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + // Set authentication in SecurityContext + // Now Spring Security knows this request is authenticated + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + } + } + + // 4. Continue the filter chain (pass request to next filter or controller) + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/SkillsForge/expensetracker/security/JwtUtil.java b/src/main/java/com/SkillsForge/expensetracker/security/JwtUtil.java new file mode 100644 index 0000000..9784219 --- /dev/null +++ b/src/main/java/com/SkillsForge/expensetracker/security/JwtUtil.java @@ -0,0 +1,137 @@ +package com.SkillsForge.expensetracker.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +/** + * Utility class for JWT (JSON Web Token) operations. Handles token generation, validation, and + * claim extraction. + */ +@Component +public class JwtUtil { + + // Secret key from application.yaml - used to sign tokens + @Value("${jwt.secret}") + private String secret; + + // Token expiration time in milliseconds from application.yaml + @Value("${jwt.expiration}") + private Long expiration; + + /** + * Generate a JWT token for a given username + * + * @param username the username to generate token for + * @return JWT token string + */ + public String generateToken(String username) { + Map claims = new HashMap<>(); + return createToken(claims, username); + } + + /** + * Create the actual JWT token with claims + * + * @param claims additional claims to include in token + * @param subject the subject (username) of the token + * @return JWT token string + */ + private String createToken(Map claims, String subject) { + return Jwts.builder() + .setClaims(claims) + .setSubject(subject) // Username + .setIssuedAt(new Date(System.currentTimeMillis())) // When token was created + .setExpiration(new Date(System.currentTimeMillis() + expiration)) // When token expires + .signWith(getSigningKey(), SignatureAlgorithm.HS256) // Sign with secret key + .compact(); + } + + /** + * Extract username from JWT token + * + * @param token JWT token + * @return username from token + */ + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + /** + * Extract expiration date from JWT token + * + * @param token JWT token + * @return expiration date + */ + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + /** + * Extract a specific claim from the token using a claims resolver function + * + * @param token JWT token + * @param claimsResolver function to extract specific claim + * @return the claim value + */ + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + /** + * Extract all claims from the JWT token + * + * @param token JWT token + * @return all claims in the token + */ + private Claims extractAllClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + /** + * Check if the token has expired + * + * @param token JWT token + * @return true if expired, false otherwise + */ + private Boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + /** + * Validate the JWT token Checks if username matches and token is not expired + * + * @param token JWT token + * @param userDetails user details to validate against + * @return true if valid, false otherwise + */ + public Boolean validateToken(String token, UserDetails userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + /** + * Get the signing key from the secret Decodes the Base64 encoded secret and creates an HMAC key + * + * @return signing key + */ + private Key getSigningKey() { + byte[] keyBytes = Decoders.BASE64.decode(secret); + return Keys.hmacShaKeyFor(keyBytes); + } +} diff --git a/src/main/java/com/SkillsForge/expensetracker/security/UserDetailsServiceImpl.java b/src/main/java/com/SkillsForge/expensetracker/security/UserDetailsServiceImpl.java new file mode 100644 index 0000000..eb60fbc --- /dev/null +++ b/src/main/java/com/SkillsForge/expensetracker/security/UserDetailsServiceImpl.java @@ -0,0 +1,85 @@ +package com.SkillsForge.expensetracker.security; + +import com.SkillsForge.expensetracker.persistence.entity.User; +import com.SkillsForge.expensetracker.persistence.repository.UserRepository; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +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; + +/** + * Implementation of Spring Security's UserDetailsService. Loads user details from database for + * authentication. + */ +@Service +@RequiredArgsConstructor +public class UserDetailsServiceImpl implements UserDetailsService { + + private final UserRepository userRepository; + + /** + * Load user by username for Spring Security authentication. This method is called by Spring + * Security during authentication. + * + * @param username the username to search for + * @return UserDetails object containing user information + * @throws UsernameNotFoundException if user not found + */ + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + // Find user in database + User user = + userRepository + .findByUsername(username) + .orElseThrow( + () -> new UsernameNotFoundException("User not found with username: " + username)); + + // Convert our User entity to Spring Security's UserDetails + return buildUserDetails(user); + } + + /** + * Build Spring Security UserDetails from our User entity + * + * @param user our User entity + * @return Spring Security UserDetails + */ + private UserDetails buildUserDetails(User user) { + return org.springframework.security.core.userdetails.User.builder() + .username(user.getUsername()) + .password(user.getPassword()) // BCrypt hashed password + .authorities(getAuthorities(user)) // Convert role to authorities + .accountExpired(!user.isAccountNonExpired()) + .accountLocked(!user.isAccountNonLocked()) + .credentialsExpired(!user.isCredentialsNonExpired()) + .disabled(!user.isEnabled()) + .build(); + } + + /** + * Get authorities (permissions) for the user based on their role Spring Security uses + * GrantedAuthority for permissions + * + * @param user the user entity + * @return collection of granted authorities + */ + private Collection getAuthorities(User user) { + Set authorities = new HashSet<>(); + + // Add role as authority (Spring Security expects "ROLE_" prefix) + authorities.add(new SimpleGrantedAuthority("ROLE_" + user.getRole().name())); + + // Add permissions from the role + user.getRole() + .getPermissions() + .forEach(permission -> authorities.add(new SimpleGrantedAuthority(permission.name()))); + + return authorities; + } +} From 17399b82a6d48f79379ef9e37497071ef25c1980 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 22:36:54 +0100 Subject: [PATCH 15/33] added extra exception classes --- .../exception/EmailAlreadyExistsException.java | 11 +++++++++++ .../exception/UsernameAlreadyExistsException.java | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/main/java/com/SkillsForge/expensetracker/exception/EmailAlreadyExistsException.java create mode 100644 src/main/java/com/SkillsForge/expensetracker/exception/UsernameAlreadyExistsException.java diff --git a/src/main/java/com/SkillsForge/expensetracker/exception/EmailAlreadyExistsException.java b/src/main/java/com/SkillsForge/expensetracker/exception/EmailAlreadyExistsException.java new file mode 100644 index 0000000..d841de4 --- /dev/null +++ b/src/main/java/com/SkillsForge/expensetracker/exception/EmailAlreadyExistsException.java @@ -0,0 +1,11 @@ +package com.SkillsForge.expensetracker.exception; + +/** + * Exception thrown when a user attempts to register with an email that already + * exists + */ +public class EmailAlreadyExistsException extends RuntimeException { + public EmailAlreadyExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/com/SkillsForge/expensetracker/exception/UsernameAlreadyExistsException.java b/src/main/java/com/SkillsForge/expensetracker/exception/UsernameAlreadyExistsException.java new file mode 100644 index 0000000..ba5ac13 --- /dev/null +++ b/src/main/java/com/SkillsForge/expensetracker/exception/UsernameAlreadyExistsException.java @@ -0,0 +1,11 @@ +package com.SkillsForge.expensetracker.exception; + +/** + * Exception thrown when a user attempts to register with a username that + * already exists + */ +public class UsernameAlreadyExistsException extends RuntimeException { + public UsernameAlreadyExistsException(String message) { + super(message); + } +} From bdf4c5d0b4d9d03935e8ca2692219241427d0198 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 22:37:19 +0100 Subject: [PATCH 16/33] updated the GlobalExceptionHandler class --- .../exception/GlobalExceptionHandler.java | 122 +++++++++++++++--- 1 file changed, 102 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/SkillsForge/expensetracker/exception/GlobalExceptionHandler.java b/src/main/java/com/SkillsForge/expensetracker/exception/GlobalExceptionHandler.java index aac3fc5..91ee712 100644 --- a/src/main/java/com/SkillsForge/expensetracker/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/SkillsForge/expensetracker/exception/GlobalExceptionHandler.java @@ -19,21 +19,19 @@ public class GlobalExceptionHandler { public ResponseEntity handleValidationException( MethodArgumentNotValidException ex, HttpServletRequest request) { - String errorMessage = - ex.getBindingResult().getFieldErrors().stream() - .map(error -> error.getField() + ": " + error.getDefaultMessage()) - .collect(Collectors.joining(", ")); + String errorMessage = ex.getBindingResult().getFieldErrors().stream() + .map(error -> error.getField() + ": " + error.getDefaultMessage()) + .collect(Collectors.joining(", ")); log.error("Validation error: {}", errorMessage); - ErrorResponse errorResponse = - ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.BAD_REQUEST.value()) - .error(HttpStatus.BAD_REQUEST.getReasonPhrase()) - .message(errorMessage) - .path(request.getRequestURI()) - .build(); + ErrorResponse errorResponse = ErrorResponse.builder() + .timestamp(LocalDateTime.now()) + .status(HttpStatus.BAD_REQUEST.value()) + .error(HttpStatus.BAD_REQUEST.getReasonPhrase()) + .message(errorMessage) + .path(request.getRequestURI()) + .build(); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse); } @@ -44,15 +42,99 @@ public ResponseEntity handleGenericException( log.error("Unexpected error occurred: ", ex); - ErrorResponse errorResponse = - ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) - .error(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()) - .message("An unexpected error occurred") - .path(request.getRequestURI()) - .build(); + ErrorResponse errorResponse = ErrorResponse.builder() + .timestamp(LocalDateTime.now()) + .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .error(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()) + .message("An unexpected error occurred") + .path(request.getRequestURI()) + .build(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); } + + @ExceptionHandler(ResourceNotFoundException.class) + public ResponseEntity handleResourceNotFound( + ResourceNotFoundException ex, HttpServletRequest request) { + + log.error("Resource not found: {}", ex.getMessage()); + + ErrorResponse errorResponse = ErrorResponse.builder() + .timestamp(LocalDateTime.now()) + .status(HttpStatus.NOT_FOUND.value()) + .error(HttpStatus.NOT_FOUND.getReasonPhrase()) + .message(ex.getMessage()) + .path(request.getRequestURI()) + .build(); + + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); + } + + @ExceptionHandler(UsernameAlreadyExistsException.class) + public ResponseEntity handleUsernameAlreadyExists( + UsernameAlreadyExistsException ex, HttpServletRequest request) { + + log.error("Username already exists: {}", ex.getMessage()); + + ErrorResponse errorResponse = ErrorResponse.builder() + .timestamp(LocalDateTime.now()) + .status(HttpStatus.CONFLICT.value()) + .error(HttpStatus.CONFLICT.getReasonPhrase()) + .message(ex.getMessage()) + .path(request.getRequestURI()) + .build(); + + return ResponseEntity.status(HttpStatus.CONFLICT).body(errorResponse); + } + + @ExceptionHandler(EmailAlreadyExistsException.class) + public ResponseEntity handleEmailAlreadyExists( + EmailAlreadyExistsException ex, HttpServletRequest request) { + + log.error("Email already exists: {}", ex.getMessage()); + + ErrorResponse errorResponse = ErrorResponse.builder() + .timestamp(LocalDateTime.now()) + .status(HttpStatus.CONFLICT.value()) + .error(HttpStatus.CONFLICT.getReasonPhrase()) + .message(ex.getMessage()) + .path(request.getRequestURI()) + .build(); + + return ResponseEntity.status(HttpStatus.CONFLICT).body(errorResponse); + } + + @ExceptionHandler(InvalidCredentialsException.class) + public ResponseEntity handleInvalidCredentials( + InvalidCredentialsException ex, HttpServletRequest request) { + + log.error("Invalid credentials: {}", ex.getMessage()); + + ErrorResponse errorResponse = ErrorResponse.builder() + .timestamp(LocalDateTime.now()) + .status(HttpStatus.UNAUTHORIZED.value()) + .error(HttpStatus.UNAUTHORIZED.getReasonPhrase()) + .message(ex.getMessage()) + .path(request.getRequestURI()) + .build(); + + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse); + } + + @ExceptionHandler(org.springframework.security.access.AccessDeniedException.class) + public ResponseEntity handleAccessDenied( + org.springframework.security.access.AccessDeniedException ex, HttpServletRequest request) { + + log.error("Access denied: {}", ex.getMessage()); + + ErrorResponse errorResponse = ErrorResponse.builder() + .timestamp(LocalDateTime.now()) + .status(HttpStatus.FORBIDDEN.value()) + .error(HttpStatus.FORBIDDEN.getReasonPhrase()) + .message("You don't have permission to access this resource") + .path(request.getRequestURI()) + .build(); + + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorResponse); + } } From de43f43e9b3b7240a24af1bc04b0604710867c47 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 23:06:48 +0100 Subject: [PATCH 17/33] added the EmailAlreadyExistsException class --- .../exception/EmailAlreadyExistsException.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/SkillsForge/expensetracker/exception/EmailAlreadyExistsException.java b/src/main/java/com/SkillsForge/expensetracker/exception/EmailAlreadyExistsException.java index d841de4..90a8725 100644 --- a/src/main/java/com/SkillsForge/expensetracker/exception/EmailAlreadyExistsException.java +++ b/src/main/java/com/SkillsForge/expensetracker/exception/EmailAlreadyExistsException.java @@ -1,11 +1,8 @@ package com.SkillsForge.expensetracker.exception; -/** - * Exception thrown when a user attempts to register with an email that already - * exists - */ +/** Exception thrown when a user attempts to register with an email that already exists */ public class EmailAlreadyExistsException extends RuntimeException { - public EmailAlreadyExistsException(String message) { - super(message); - } + public EmailAlreadyExistsException(String message) { + super(message); + } } From 4cd934f880dc94b42930a6d6eac9c61c5d1abfeb Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 23:07:05 +0100 Subject: [PATCH 18/33] added the UsernameAlreadyExistsException class --- .../exception/UsernameAlreadyExistsException.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/SkillsForge/expensetracker/exception/UsernameAlreadyExistsException.java b/src/main/java/com/SkillsForge/expensetracker/exception/UsernameAlreadyExistsException.java index ba5ac13..3e04cfa 100644 --- a/src/main/java/com/SkillsForge/expensetracker/exception/UsernameAlreadyExistsException.java +++ b/src/main/java/com/SkillsForge/expensetracker/exception/UsernameAlreadyExistsException.java @@ -1,11 +1,8 @@ package com.SkillsForge.expensetracker.exception; -/** - * Exception thrown when a user attempts to register with a username that - * already exists - */ +/** Exception thrown when a user attempts to register with a username that already exists */ public class UsernameAlreadyExistsException extends RuntimeException { - public UsernameAlreadyExistsException(String message) { - super(message); - } + public UsernameAlreadyExistsException(String message) { + super(message); + } } From dcb78005a6a9b959e4478c08b4b01e895a40ee83 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 23:07:25 +0100 Subject: [PATCH 19/33] added the InvalidCredentialException class --- .../exception/InvalidCredentialsException.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/com/SkillsForge/expensetracker/exception/InvalidCredentialsException.java diff --git a/src/main/java/com/SkillsForge/expensetracker/exception/InvalidCredentialsException.java b/src/main/java/com/SkillsForge/expensetracker/exception/InvalidCredentialsException.java new file mode 100644 index 0000000..ee77e6e --- /dev/null +++ b/src/main/java/com/SkillsForge/expensetracker/exception/InvalidCredentialsException.java @@ -0,0 +1,8 @@ +package com.SkillsForge.expensetracker.exception; + +/** Exception thrown when authentication fails due to invalid credentials */ +public class InvalidCredentialsException extends RuntimeException { + public InvalidCredentialsException(String message) { + super(message); + } +} From 4131830725ca570caeb1c71e5e1ce9c50efb7f80 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 23:07:50 +0100 Subject: [PATCH 20/33] modified the GlobalExceptionHandler --- .../exception/GlobalExceptionHandler.java | 112 ++++++++++-------- 1 file changed, 60 insertions(+), 52 deletions(-) diff --git a/src/main/java/com/SkillsForge/expensetracker/exception/GlobalExceptionHandler.java b/src/main/java/com/SkillsForge/expensetracker/exception/GlobalExceptionHandler.java index 91ee712..6f2930c 100644 --- a/src/main/java/com/SkillsForge/expensetracker/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/SkillsForge/expensetracker/exception/GlobalExceptionHandler.java @@ -19,19 +19,21 @@ public class GlobalExceptionHandler { public ResponseEntity handleValidationException( MethodArgumentNotValidException ex, HttpServletRequest request) { - String errorMessage = ex.getBindingResult().getFieldErrors().stream() - .map(error -> error.getField() + ": " + error.getDefaultMessage()) - .collect(Collectors.joining(", ")); + String errorMessage = + ex.getBindingResult().getFieldErrors().stream() + .map(error -> error.getField() + ": " + error.getDefaultMessage()) + .collect(Collectors.joining(", ")); log.error("Validation error: {}", errorMessage); - ErrorResponse errorResponse = ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.BAD_REQUEST.value()) - .error(HttpStatus.BAD_REQUEST.getReasonPhrase()) - .message(errorMessage) - .path(request.getRequestURI()) - .build(); + ErrorResponse errorResponse = + ErrorResponse.builder() + .timestamp(LocalDateTime.now()) + .status(HttpStatus.BAD_REQUEST.value()) + .error(HttpStatus.BAD_REQUEST.getReasonPhrase()) + .message(errorMessage) + .path(request.getRequestURI()) + .build(); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse); } @@ -42,13 +44,14 @@ public ResponseEntity handleGenericException( log.error("Unexpected error occurred: ", ex); - ErrorResponse errorResponse = ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) - .error(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()) - .message("An unexpected error occurred") - .path(request.getRequestURI()) - .build(); + ErrorResponse errorResponse = + ErrorResponse.builder() + .timestamp(LocalDateTime.now()) + .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .error(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()) + .message("An unexpected error occurred") + .path(request.getRequestURI()) + .build(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); } @@ -59,13 +62,14 @@ public ResponseEntity handleResourceNotFound( log.error("Resource not found: {}", ex.getMessage()); - ErrorResponse errorResponse = ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.NOT_FOUND.value()) - .error(HttpStatus.NOT_FOUND.getReasonPhrase()) - .message(ex.getMessage()) - .path(request.getRequestURI()) - .build(); + ErrorResponse errorResponse = + ErrorResponse.builder() + .timestamp(LocalDateTime.now()) + .status(HttpStatus.NOT_FOUND.value()) + .error(HttpStatus.NOT_FOUND.getReasonPhrase()) + .message(ex.getMessage()) + .path(request.getRequestURI()) + .build(); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); } @@ -76,13 +80,14 @@ public ResponseEntity handleUsernameAlreadyExists( log.error("Username already exists: {}", ex.getMessage()); - ErrorResponse errorResponse = ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.CONFLICT.value()) - .error(HttpStatus.CONFLICT.getReasonPhrase()) - .message(ex.getMessage()) - .path(request.getRequestURI()) - .build(); + ErrorResponse errorResponse = + ErrorResponse.builder() + .timestamp(LocalDateTime.now()) + .status(HttpStatus.CONFLICT.value()) + .error(HttpStatus.CONFLICT.getReasonPhrase()) + .message(ex.getMessage()) + .path(request.getRequestURI()) + .build(); return ResponseEntity.status(HttpStatus.CONFLICT).body(errorResponse); } @@ -93,13 +98,14 @@ public ResponseEntity handleEmailAlreadyExists( log.error("Email already exists: {}", ex.getMessage()); - ErrorResponse errorResponse = ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.CONFLICT.value()) - .error(HttpStatus.CONFLICT.getReasonPhrase()) - .message(ex.getMessage()) - .path(request.getRequestURI()) - .build(); + ErrorResponse errorResponse = + ErrorResponse.builder() + .timestamp(LocalDateTime.now()) + .status(HttpStatus.CONFLICT.value()) + .error(HttpStatus.CONFLICT.getReasonPhrase()) + .message(ex.getMessage()) + .path(request.getRequestURI()) + .build(); return ResponseEntity.status(HttpStatus.CONFLICT).body(errorResponse); } @@ -110,13 +116,14 @@ public ResponseEntity handleInvalidCredentials( log.error("Invalid credentials: {}", ex.getMessage()); - ErrorResponse errorResponse = ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.UNAUTHORIZED.value()) - .error(HttpStatus.UNAUTHORIZED.getReasonPhrase()) - .message(ex.getMessage()) - .path(request.getRequestURI()) - .build(); + ErrorResponse errorResponse = + ErrorResponse.builder() + .timestamp(LocalDateTime.now()) + .status(HttpStatus.UNAUTHORIZED.value()) + .error(HttpStatus.UNAUTHORIZED.getReasonPhrase()) + .message(ex.getMessage()) + .path(request.getRequestURI()) + .build(); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse); } @@ -127,13 +134,14 @@ public ResponseEntity handleAccessDenied( log.error("Access denied: {}", ex.getMessage()); - ErrorResponse errorResponse = ErrorResponse.builder() - .timestamp(LocalDateTime.now()) - .status(HttpStatus.FORBIDDEN.value()) - .error(HttpStatus.FORBIDDEN.getReasonPhrase()) - .message("You don't have permission to access this resource") - .path(request.getRequestURI()) - .build(); + ErrorResponse errorResponse = + ErrorResponse.builder() + .timestamp(LocalDateTime.now()) + .status(HttpStatus.FORBIDDEN.value()) + .error(HttpStatus.FORBIDDEN.getReasonPhrase()) + .message("You don't have permission to access this resource") + .path(request.getRequestURI()) + .build(); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorResponse); } From cfd0087f503f9bd1d9427d07d2d2096318dc6c0a Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 23:08:04 +0100 Subject: [PATCH 21/33] scaffolded UserService class --- .../expensetracker/service/UserService.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/main/java/com/SkillsForge/expensetracker/service/UserService.java diff --git a/src/main/java/com/SkillsForge/expensetracker/service/UserService.java b/src/main/java/com/SkillsForge/expensetracker/service/UserService.java new file mode 100644 index 0000000..98d05da --- /dev/null +++ b/src/main/java/com/SkillsForge/expensetracker/service/UserService.java @@ -0,0 +1,35 @@ +package com.SkillsForge.expensetracker.service; + +import com.SkillsForge.expensetracker.dto.AuthResponse; +import com.SkillsForge.expensetracker.dto.LoginRequest; +import com.SkillsForge.expensetracker.dto.SignupRequest; +import com.SkillsForge.expensetracker.dto.UserDto; + +/** + * Service interface for user authentication and management + */ +public interface UserService { + + /** + * Register a new user account + * + * @param request signup details (username, email, password) + * @return authentication response with JWT token + */ + AuthResponse signup(SignupRequest request); + + /** + * Authenticate user and generate JWT token + * + * @param request login credentials (username, password) + * @return authentication response with JWT token + */ + AuthResponse login(LoginRequest request); + + /** + * Get the currently authenticated user + * + * @return user details without password + */ + UserDto getCurrentUser(); +} From 8cb0b4371b3c0578d3991fac209779c82eafda65 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 23:08:20 +0100 Subject: [PATCH 22/33] implemented the userService --- .../service/UserServiceImpl.java | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 src/main/java/com/SkillsForge/expensetracker/service/UserServiceImpl.java diff --git a/src/main/java/com/SkillsForge/expensetracker/service/UserServiceImpl.java b/src/main/java/com/SkillsForge/expensetracker/service/UserServiceImpl.java new file mode 100644 index 0000000..7560478 --- /dev/null +++ b/src/main/java/com/SkillsForge/expensetracker/service/UserServiceImpl.java @@ -0,0 +1,159 @@ +package com.SkillsForge.expensetracker.service; + +import com.SkillsForge.expensetracker.app.enums.Role; +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.exception.EmailAlreadyExistsException; +import com.SkillsForge.expensetracker.exception.InvalidCredentialsException; +import com.SkillsForge.expensetracker.exception.UsernameAlreadyExistsException; +import com.SkillsForge.expensetracker.persistence.entity.User; +import com.SkillsForge.expensetracker.persistence.repository.UserRepository; +import com.SkillsForge.expensetracker.security.JwtUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Implementation of UserService for authentication and user management + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class UserServiceImpl implements UserService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final AuthenticationManager authenticationManager; + private final JwtUtil jwtUtil; + + /** + * Register a new user with default USER role + * + * @param request signup request with username, email, password + * @return authentication response with JWT token + * @throws UsernameAlreadyExistsException if username already taken + * @throws EmailAlreadyExistsException if email already registered + */ + @Override + @Transactional + public AuthResponse signup(SignupRequest request) { + log.info("Attempting to register new user: {}", request.getUsername()); + + // Check if username already exists + if (userRepository.existsByUsername(request.getUsername())) { + log.warn("Signup failed: username already exists - {}", request.getUsername()); + throw new UsernameAlreadyExistsException( + "Username already taken: " + request.getUsername()); + } + + // Check if email already exists + if (userRepository.existsByEmail(request.getEmail())) { + log.warn("Signup failed: email already exists - {}", request.getEmail()); + throw new EmailAlreadyExistsException("Email already registered: " + request.getEmail()); + } + + // Create new user with hashed password + User user = User.builder() + .username(request.getUsername()) + .email(request.getEmail()) + .password(passwordEncoder.encode(request.getPassword())) // Hash password with BCrypt + .role(Role.USER) // Default role + .enabled(true) + .accountNonExpired(true) + .accountNonLocked(true) + .credentialsNonExpired(true) + .build(); + + // Save user to database + User savedUser = userRepository.save(user); + log.info("Successfully registered new user with ID: {}", savedUser.getId()); + + // Generate JWT token for automatic login + String token = jwtUtil.generateToken(savedUser.getUsername()); + + // Build and return authentication response + return AuthResponse.builder() + .token(token) + .type("Bearer") + .id(savedUser.getId()) + .username(savedUser.getUsername()) + .email(savedUser.getEmail()) + .role(savedUser.getRole()) + .build(); + } + + /** + * Authenticate user with credentials and generate JWT token + * + * @param request login request with username and password + * @return authentication response with JWT token + * @throws InvalidCredentialsException if credentials are invalid + */ + @Override + public AuthResponse login(LoginRequest request) { + log.info("Login attempt for user: {}", request.getUsername()); + + try { + // Authenticate user credentials + // Spring Security will compare plain password with BCrypt hash + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); + } catch (AuthenticationException e) { + log.warn("Login failed for user: {} - Invalid credentials", request.getUsername()); + throw new InvalidCredentialsException("Invalid username or password"); + } + + // Load user from database (authentication successful) + User user = userRepository + .findByUsername(request.getUsername()) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + log.info("User {} logged in successfully", user.getUsername()); + + // Generate JWT token + String token = jwtUtil.generateToken(user.getUsername()); + + // Build and return authentication response + return AuthResponse.builder() + .token(token) + .type("Bearer") + .id(user.getId()) + .username(user.getUsername()) + .email(user.getEmail()) + .role(user.getRole()) + .build(); + } + + /** + * Get currently authenticated user details + * + * @return user DTO without password + * @throws UsernameNotFoundException if user not found + */ + @Override + public UserDto getCurrentUser() { + // Get authentication from SecurityContext (set by JWT filter) + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String username = authentication.getName(); + + log.debug("Fetching current user: {}", username); + + // Load user from database + User user = userRepository + .findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); + + // Convert to DTO (excludes password) + return UserDto.from(user); + } +} From e9eb73c52dd605bb5f97590adff72a7dae181bb5 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 23:11:10 +0100 Subject: [PATCH 23/33] removed unnecessary comments and implemented AuthController --- .../controller/AuthController.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/main/java/com/SkillsForge/expensetracker/controller/AuthController.java diff --git a/src/main/java/com/SkillsForge/expensetracker/controller/AuthController.java b/src/main/java/com/SkillsForge/expensetracker/controller/AuthController.java new file mode 100644 index 0000000..ae67fbf --- /dev/null +++ b/src/main/java/com/SkillsForge/expensetracker/controller/AuthController.java @@ -0,0 +1,43 @@ +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.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 signup(@Valid @RequestBody SignupRequest request) { + AuthResponse response = userService.signup(request); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } + + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody LoginRequest request) { + AuthResponse response = userService.login(request); + return ResponseEntity.ok(response); + } + + @GetMapping("/me") + public ResponseEntity getCurrentUser() { + UserDto user = userService.getCurrentUser(); + return ResponseEntity.ok(user); + } +} From 28a163f7592f8f211c00803f91bfd80d7267a496 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 23:11:33 +0100 Subject: [PATCH 24/33] implemented auth controller and removed redudant comments --- .../EmailAlreadyExistsException.java | 2 +- .../InvalidCredentialsException.java | 2 +- .../UsernameAlreadyExistsException.java | 2 +- .../expensetracker/service/UserService.java | 22 ++-------------- .../service/UserServiceImpl.java | 25 +------------------ 5 files changed, 6 insertions(+), 47 deletions(-) diff --git a/src/main/java/com/SkillsForge/expensetracker/exception/EmailAlreadyExistsException.java b/src/main/java/com/SkillsForge/expensetracker/exception/EmailAlreadyExistsException.java index 90a8725..9203e7e 100644 --- a/src/main/java/com/SkillsForge/expensetracker/exception/EmailAlreadyExistsException.java +++ b/src/main/java/com/SkillsForge/expensetracker/exception/EmailAlreadyExistsException.java @@ -1,6 +1,6 @@ package com.SkillsForge.expensetracker.exception; -/** Exception thrown when a user attempts to register with an email that already exists */ + public class EmailAlreadyExistsException extends RuntimeException { public EmailAlreadyExistsException(String message) { super(message); diff --git a/src/main/java/com/SkillsForge/expensetracker/exception/InvalidCredentialsException.java b/src/main/java/com/SkillsForge/expensetracker/exception/InvalidCredentialsException.java index ee77e6e..0aed7af 100644 --- a/src/main/java/com/SkillsForge/expensetracker/exception/InvalidCredentialsException.java +++ b/src/main/java/com/SkillsForge/expensetracker/exception/InvalidCredentialsException.java @@ -1,6 +1,6 @@ package com.SkillsForge.expensetracker.exception; -/** Exception thrown when authentication fails due to invalid credentials */ + public class InvalidCredentialsException extends RuntimeException { public InvalidCredentialsException(String message) { super(message); diff --git a/src/main/java/com/SkillsForge/expensetracker/exception/UsernameAlreadyExistsException.java b/src/main/java/com/SkillsForge/expensetracker/exception/UsernameAlreadyExistsException.java index 3e04cfa..8fa10c7 100644 --- a/src/main/java/com/SkillsForge/expensetracker/exception/UsernameAlreadyExistsException.java +++ b/src/main/java/com/SkillsForge/expensetracker/exception/UsernameAlreadyExistsException.java @@ -1,6 +1,6 @@ package com.SkillsForge.expensetracker.exception; -/** Exception thrown when a user attempts to register with a username that already exists */ + public class UsernameAlreadyExistsException extends RuntimeException { public UsernameAlreadyExistsException(String message) { super(message); diff --git a/src/main/java/com/SkillsForge/expensetracker/service/UserService.java b/src/main/java/com/SkillsForge/expensetracker/service/UserService.java index 98d05da..138b85c 100644 --- a/src/main/java/com/SkillsForge/expensetracker/service/UserService.java +++ b/src/main/java/com/SkillsForge/expensetracker/service/UserService.java @@ -5,31 +5,13 @@ import com.SkillsForge.expensetracker.dto.SignupRequest; import com.SkillsForge.expensetracker.dto.UserDto; -/** - * Service interface for user authentication and management - */ public interface UserService { - /** - * Register a new user account - * - * @param request signup details (username, email, password) - * @return authentication response with JWT token - */ + AuthResponse signup(SignupRequest request); - /** - * Authenticate user and generate JWT token - * - * @param request login credentials (username, password) - * @return authentication response with JWT token - */ + AuthResponse login(LoginRequest request); - /** - * Get the currently authenticated user - * - * @return user details without password - */ UserDto getCurrentUser(); } diff --git a/src/main/java/com/SkillsForge/expensetracker/service/UserServiceImpl.java b/src/main/java/com/SkillsForge/expensetracker/service/UserServiceImpl.java index 7560478..029ec31 100644 --- a/src/main/java/com/SkillsForge/expensetracker/service/UserServiceImpl.java +++ b/src/main/java/com/SkillsForge/expensetracker/service/UserServiceImpl.java @@ -23,9 +23,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -/** - * Implementation of UserService for authentication and user management - */ @Slf4j @Service @RequiredArgsConstructor @@ -36,14 +33,6 @@ public class UserServiceImpl implements UserService { private final AuthenticationManager authenticationManager; private final JwtUtil jwtUtil; - /** - * Register a new user with default USER role - * - * @param request signup request with username, email, password - * @return authentication response with JWT token - * @throws UsernameAlreadyExistsException if username already taken - * @throws EmailAlreadyExistsException if email already registered - */ @Override @Transactional public AuthResponse signup(SignupRequest request) { @@ -92,13 +81,6 @@ public AuthResponse signup(SignupRequest request) { .build(); } - /** - * Authenticate user with credentials and generate JWT token - * - * @param request login request with username and password - * @return authentication response with JWT token - * @throws InvalidCredentialsException if credentials are invalid - */ @Override public AuthResponse login(LoginRequest request) { log.info("Login attempt for user: {}", request.getUsername()); @@ -134,12 +116,7 @@ public AuthResponse login(LoginRequest request) { .build(); } - /** - * Get currently authenticated user details - * - * @return user DTO without password - * @throws UsernameNotFoundException if user not found - */ + @Override public UserDto getCurrentUser() { // Get authentication from SecurityContext (set by JWT filter) From 57a3a347fe77e457ff82537b453ddeb54e0eb0b9 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Sun, 8 Feb 2026 23:43:13 +0100 Subject: [PATCH 25/33] fixed bug with app not starting --- .../expensetracker/app/Config.java | 2 +- .../controller/AuthController.java | 33 ++- .../EmailAlreadyExistsException.java | 1 - .../InvalidCredentialsException.java | 1 - .../UsernameAlreadyExistsException.java | 1 - .../security/JwtAuthenticationFilter.java | 24 +- .../expensetracker/security/JwtUtil.java | 130 +++-------- .../security/UserDetailsServiceImpl.java | 25 -- .../expensetracker/service/UserService.java | 8 +- .../service/UserServiceImpl.java | 217 ++++++++++-------- src/main/resources/application.yaml | 8 +- 11 files changed, 183 insertions(+), 267 deletions(-) diff --git a/src/main/java/com/SkillsForge/expensetracker/app/Config.java b/src/main/java/com/SkillsForge/expensetracker/app/Config.java index a5bda1b..90b861c 100644 --- a/src/main/java/com/SkillsForge/expensetracker/app/Config.java +++ b/src/main/java/com/SkillsForge/expensetracker/app/Config.java @@ -47,7 +47,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti auth -> auth // Public endpoints - anyone can access - .requestMatchers("/api/auth/**") + .requestMatchers("/api/v1/auth/**") .permitAll() // All other endpoints require authentication diff --git a/src/main/java/com/SkillsForge/expensetracker/controller/AuthController.java b/src/main/java/com/SkillsForge/expensetracker/controller/AuthController.java index ae67fbf..be61f62 100644 --- a/src/main/java/com/SkillsForge/expensetracker/controller/AuthController.java +++ b/src/main/java/com/SkillsForge/expensetracker/controller/AuthController.java @@ -15,29 +15,28 @@ 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; + private final UserService userService; - @PostMapping("/signup") - public ResponseEntity signup(@Valid @RequestBody SignupRequest request) { - AuthResponse response = userService.signup(request); - return new ResponseEntity<>(response, HttpStatus.CREATED); - } + @PostMapping("/signup") + public ResponseEntity signup(@Valid @RequestBody SignupRequest request) { + AuthResponse response = userService.signup(request); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } - @PostMapping("/login") - public ResponseEntity login(@Valid @RequestBody LoginRequest request) { - AuthResponse response = userService.login(request); - return ResponseEntity.ok(response); - } + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody LoginRequest request) { + AuthResponse response = userService.login(request); + return ResponseEntity.ok(response); + } - @GetMapping("/me") - public ResponseEntity getCurrentUser() { - UserDto user = userService.getCurrentUser(); - return ResponseEntity.ok(user); - } + @GetMapping("/me") + public ResponseEntity getCurrentUser() { + UserDto user = userService.getCurrentUser(); + return ResponseEntity.ok(user); + } } diff --git a/src/main/java/com/SkillsForge/expensetracker/exception/EmailAlreadyExistsException.java b/src/main/java/com/SkillsForge/expensetracker/exception/EmailAlreadyExistsException.java index 9203e7e..b34e47f 100644 --- a/src/main/java/com/SkillsForge/expensetracker/exception/EmailAlreadyExistsException.java +++ b/src/main/java/com/SkillsForge/expensetracker/exception/EmailAlreadyExistsException.java @@ -1,6 +1,5 @@ package com.SkillsForge.expensetracker.exception; - public class EmailAlreadyExistsException extends RuntimeException { public EmailAlreadyExistsException(String message) { super(message); diff --git a/src/main/java/com/SkillsForge/expensetracker/exception/InvalidCredentialsException.java b/src/main/java/com/SkillsForge/expensetracker/exception/InvalidCredentialsException.java index 0aed7af..ef4d69e 100644 --- a/src/main/java/com/SkillsForge/expensetracker/exception/InvalidCredentialsException.java +++ b/src/main/java/com/SkillsForge/expensetracker/exception/InvalidCredentialsException.java @@ -1,6 +1,5 @@ package com.SkillsForge.expensetracker.exception; - public class InvalidCredentialsException extends RuntimeException { public InvalidCredentialsException(String message) { super(message); diff --git a/src/main/java/com/SkillsForge/expensetracker/exception/UsernameAlreadyExistsException.java b/src/main/java/com/SkillsForge/expensetracker/exception/UsernameAlreadyExistsException.java index 8fa10c7..a77f2cb 100644 --- a/src/main/java/com/SkillsForge/expensetracker/exception/UsernameAlreadyExistsException.java +++ b/src/main/java/com/SkillsForge/expensetracker/exception/UsernameAlreadyExistsException.java @@ -1,6 +1,5 @@ package com.SkillsForge.expensetracker.exception; - public class UsernameAlreadyExistsException extends RuntimeException { public UsernameAlreadyExistsException(String message) { super(message); diff --git a/src/main/java/com/SkillsForge/expensetracker/security/JwtAuthenticationFilter.java b/src/main/java/com/SkillsForge/expensetracker/security/JwtAuthenticationFilter.java index b3f900f..69e6a4e 100644 --- a/src/main/java/com/SkillsForge/expensetracker/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/SkillsForge/expensetracker/security/JwtAuthenticationFilter.java @@ -14,10 +14,6 @@ import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; -/** - * JWT Authentication Filter that intercepts every HTTP request. Validates JWT token and sets - * authentication in Spring Security context. - */ @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { @@ -25,16 +21,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final UserDetailsServiceImpl userDetailsService; - /** - * Filter method that runs once per request. Extracts and validates JWT token, then sets - * authentication. - * - * @param request HTTP request - * @param response HTTP response - * @param filterChain filter chain to continue processing - * @throws ServletException if servlet error occurs - * @throws IOException if I/O error occurs - */ @Override protected void doFilterInternal( @NonNull HttpServletRequest request, @@ -42,13 +28,13 @@ protected void doFilterInternal( @NonNull FilterChain filterChain) throws ServletException, IOException { - // 1. Extract the Authorization header from the request + // Extract the Authorization header from the request final String authorizationHeader = request.getHeader("Authorization"); String username = null; String jwt = null; - // 2. Check if header exists and starts with "Bearer " + // Check if header exists and starts with "Bearer " if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { jwt = authorizationHeader.substring(7); // Remove "Bearer " prefix to get token try { @@ -59,14 +45,14 @@ protected void doFilterInternal( } } - // 3. If we have a username and no authentication exists in SecurityContext + // If we have a username and no authentication exists in SecurityContext if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { // Load user details from database UserDetails userDetails = userDetailsService.loadUserByUsername(username); // Validate the token - if (jwtUtil.validateToken(jwt, userDetails)) { + if (jwtUtil.isTokenValid(jwt, userDetails)) { // Token is valid - create authentication object UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( @@ -81,7 +67,7 @@ protected void doFilterInternal( } } - // 4. Continue the filter chain (pass request to next filter or controller) + // Continue the filter chain (pass request to next filter or controller) filterChain.doFilter(request, response); } } diff --git a/src/main/java/com/SkillsForge/expensetracker/security/JwtUtil.java b/src/main/java/com/SkillsForge/expensetracker/security/JwtUtil.java index 9784219..b7a6259 100644 --- a/src/main/java/com/SkillsForge/expensetracker/security/JwtUtil.java +++ b/src/main/java/com/SkillsForge/expensetracker/security/JwtUtil.java @@ -2,135 +2,77 @@ import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; -import java.security.Key; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.function.Function; +import javax.crypto.SecretKey; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; -/** - * Utility class for JWT (JSON Web Token) operations. Handles token generation, validation, and - * claim extraction. - */ @Component public class JwtUtil { - // Secret key from application.yaml - used to sign tokens @Value("${jwt.secret}") private String secret; - // Token expiration time in milliseconds from application.yaml @Value("${jwt.expiration}") - private Long expiration; + private long jwtExpiration; - /** - * Generate a JWT token for a given username - * - * @param username the username to generate token for - * @return JWT token string - */ - public String generateToken(String username) { - Map claims = new HashMap<>(); - return createToken(claims, username); + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); } - /** - * Create the actual JWT token with claims - * - * @param claims additional claims to include in token - * @param subject the subject (username) of the token - * @return JWT token string - */ - private String createToken(Map claims, String subject) { - return Jwts.builder() - .setClaims(claims) - .setSubject(subject) // Username - .setIssuedAt(new Date(System.currentTimeMillis())) // When token was created - .setExpiration(new Date(System.currentTimeMillis() + expiration)) // When token expires - .signWith(getSigningKey(), SignatureAlgorithm.HS256) // Sign with secret key - .compact(); + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); } - /** - * Extract username from JWT token - * - * @param token JWT token - * @return username from token - */ - public String extractUsername(String token) { - return extractClaim(token, Claims::getSubject); + public String generateToken(UserDetails userDetails) { + return generateToken(new HashMap<>(), userDetails); } - /** - * Extract expiration date from JWT token - * - * @param token JWT token - * @return expiration date - */ - public Date extractExpiration(String token) { - return extractClaim(token, Claims::getExpiration); + // Renamed to conform to modern standards (using UserDetails) + public String generateToken(Map extraClaims, UserDetails userDetails) { + return buildToken(extraClaims, userDetails, jwtExpiration); } - /** - * Extract a specific claim from the token using a claims resolver function - * - * @param token JWT token - * @param claimsResolver function to extract specific claim - * @return the claim value - */ - public T extractClaim(String token, Function claimsResolver) { - final Claims claims = extractAllClaims(token); - return claimsResolver.apply(claims); + private String buildToken( + Map extraClaims, UserDetails userDetails, long expiration) { + return Jwts.builder() + .claims(extraClaims) // New method: 'claims' instead of 'setClaims' + .subject(userDetails.getUsername()) // New method: 'subject' instead of 'setSubject' + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSignInKey(), Jwts.SIG.HS256) // MODERN: Use Jwts.SIG + .compact(); } - /** - * Extract all claims from the JWT token - * - * @param token JWT token - * @return all claims in the token - */ - private Claims extractAllClaims(String token) { - return Jwts.parserBuilder() - .setSigningKey(getSigningKey()) - .build() - .parseClaimsJws(token) - .getBody(); + public boolean isTokenValid(String token, UserDetails userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername())) && !isTokenExpired(token); } - /** - * Check if the token has expired - * - * @param token JWT token - * @return true if expired, false otherwise - */ - private Boolean isTokenExpired(String token) { + private boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } - /** - * Validate the JWT token Checks if username matches and token is not expired - * - * @param token JWT token - * @param userDetails user details to validate against - * @return true if valid, false otherwise - */ - public Boolean validateToken(String token, UserDetails userDetails) { - final String username = extractUsername(token); - return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + private Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith(getSignInKey()) // MODERN: 'verifyWith' checks the signature + .build() + .parseSignedClaims(token) // MODERN: 'parseSignedClaims' instead of 'parseClaimsJws' + .getPayload(); // MODERN: 'getPayload' instead of 'getBody' } - /** - * Get the signing key from the secret Decodes the Base64 encoded secret and creates an HMAC key - * - * @return signing key - */ - private Key getSigningKey() { + private SecretKey getSignInKey() { byte[] keyBytes = Decoders.BASE64.decode(secret); return Keys.hmacShaKeyFor(keyBytes); } diff --git a/src/main/java/com/SkillsForge/expensetracker/security/UserDetailsServiceImpl.java b/src/main/java/com/SkillsForge/expensetracker/security/UserDetailsServiceImpl.java index eb60fbc..84435d2 100644 --- a/src/main/java/com/SkillsForge/expensetracker/security/UserDetailsServiceImpl.java +++ b/src/main/java/com/SkillsForge/expensetracker/security/UserDetailsServiceImpl.java @@ -13,24 +13,12 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; -/** - * Implementation of Spring Security's UserDetailsService. Loads user details from database for - * authentication. - */ @Service @RequiredArgsConstructor public class UserDetailsServiceImpl implements UserDetailsService { private final UserRepository userRepository; - /** - * Load user by username for Spring Security authentication. This method is called by Spring - * Security during authentication. - * - * @param username the username to search for - * @return UserDetails object containing user information - * @throws UsernameNotFoundException if user not found - */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // Find user in database @@ -44,12 +32,6 @@ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundEx return buildUserDetails(user); } - /** - * Build Spring Security UserDetails from our User entity - * - * @param user our User entity - * @return Spring Security UserDetails - */ private UserDetails buildUserDetails(User user) { return org.springframework.security.core.userdetails.User.builder() .username(user.getUsername()) @@ -62,13 +44,6 @@ private UserDetails buildUserDetails(User user) { .build(); } - /** - * Get authorities (permissions) for the user based on their role Spring Security uses - * GrantedAuthority for permissions - * - * @param user the user entity - * @return collection of granted authorities - */ private Collection getAuthorities(User user) { Set authorities = new HashSet<>(); diff --git a/src/main/java/com/SkillsForge/expensetracker/service/UserService.java b/src/main/java/com/SkillsForge/expensetracker/service/UserService.java index 138b85c..db3848e 100644 --- a/src/main/java/com/SkillsForge/expensetracker/service/UserService.java +++ b/src/main/java/com/SkillsForge/expensetracker/service/UserService.java @@ -7,11 +7,9 @@ public interface UserService { + AuthResponse signup(SignupRequest request); - AuthResponse signup(SignupRequest request); + AuthResponse login(LoginRequest request); - - AuthResponse login(LoginRequest request); - - UserDto getCurrentUser(); + UserDto getCurrentUser(); } diff --git a/src/main/java/com/SkillsForge/expensetracker/service/UserServiceImpl.java b/src/main/java/com/SkillsForge/expensetracker/service/UserServiceImpl.java index 029ec31..b3f167c 100644 --- a/src/main/java/com/SkillsForge/expensetracker/service/UserServiceImpl.java +++ b/src/main/java/com/SkillsForge/expensetracker/service/UserServiceImpl.java @@ -28,109 +28,126 @@ @RequiredArgsConstructor public class UserServiceImpl implements UserService { - private final UserRepository userRepository; - private final PasswordEncoder passwordEncoder; - private final AuthenticationManager authenticationManager; - private final JwtUtil jwtUtil; - - @Override - @Transactional - public AuthResponse signup(SignupRequest request) { - log.info("Attempting to register new user: {}", request.getUsername()); - - // Check if username already exists - if (userRepository.existsByUsername(request.getUsername())) { - log.warn("Signup failed: username already exists - {}", request.getUsername()); - throw new UsernameAlreadyExistsException( - "Username already taken: " + request.getUsername()); - } - - // Check if email already exists - if (userRepository.existsByEmail(request.getEmail())) { - log.warn("Signup failed: email already exists - {}", request.getEmail()); - throw new EmailAlreadyExistsException("Email already registered: " + request.getEmail()); - } - - // Create new user with hashed password - User user = User.builder() - .username(request.getUsername()) - .email(request.getEmail()) - .password(passwordEncoder.encode(request.getPassword())) // Hash password with BCrypt - .role(Role.USER) // Default role - .enabled(true) - .accountNonExpired(true) - .accountNonLocked(true) - .credentialsNonExpired(true) - .build(); - - // Save user to database - User savedUser = userRepository.save(user); - log.info("Successfully registered new user with ID: {}", savedUser.getId()); - - // Generate JWT token for automatic login - String token = jwtUtil.generateToken(savedUser.getUsername()); - - // Build and return authentication response - return AuthResponse.builder() - .token(token) - .type("Bearer") - .id(savedUser.getId()) - .username(savedUser.getUsername()) - .email(savedUser.getEmail()) - .role(savedUser.getRole()) - .build(); + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final AuthenticationManager authenticationManager; + private final JwtUtil jwtUtil; + + @Override + @Transactional + public AuthResponse signup(SignupRequest request) { + log.info("Attempting to register new user: {}", request.getUsername()); + + // Check if username already exists + if (userRepository.existsByUsername(request.getUsername())) { + log.warn("Signup failed: username already exists - {}", request.getUsername()); + throw new UsernameAlreadyExistsException("Username already taken: " + request.getUsername()); } - @Override - public AuthResponse login(LoginRequest request) { - log.info("Login attempt for user: {}", request.getUsername()); - - try { - // Authenticate user credentials - // Spring Security will compare plain password with BCrypt hash - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); - } catch (AuthenticationException e) { - log.warn("Login failed for user: {} - Invalid credentials", request.getUsername()); - throw new InvalidCredentialsException("Invalid username or password"); - } - - // Load user from database (authentication successful) - User user = userRepository - .findByUsername(request.getUsername()) - .orElseThrow(() -> new UsernameNotFoundException("User not found")); - - log.info("User {} logged in successfully", user.getUsername()); - - // Generate JWT token - String token = jwtUtil.generateToken(user.getUsername()); - - // Build and return authentication response - return AuthResponse.builder() - .token(token) - .type("Bearer") - .id(user.getId()) - .username(user.getUsername()) - .email(user.getEmail()) - .role(user.getRole()) - .build(); + // Check if email already exists + if (userRepository.existsByEmail(request.getEmail())) { + log.warn("Signup failed: email already exists - {}", request.getEmail()); + throw new EmailAlreadyExistsException("Email already registered: " + request.getEmail()); } - - @Override - public UserDto getCurrentUser() { - // Get authentication from SecurityContext (set by JWT filter) - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String username = authentication.getName(); - - log.debug("Fetching current user: {}", username); - - // Load user from database - User user = userRepository - .findByUsername(username) - .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); - - // Convert to DTO (excludes password) - return UserDto.from(user); + // Create new user with hashed password + User user = + User.builder() + .username(request.getUsername()) + .email(request.getEmail()) + .password(passwordEncoder.encode(request.getPassword())) // Hash password with BCrypt + .role(Role.USER) // Default role + .enabled(true) + .accountNonExpired(true) + .accountNonLocked(true) + .credentialsNonExpired(true) + .build(); + + // Save user to database + User savedUser = userRepository.save(user); + log.info("Successfully registered new user with ID: {}", savedUser.getId()); + + // Create UserDetails for token generation + org.springframework.security.core.userdetails.UserDetails userDetails = + org.springframework.security.core.userdetails.User.builder() + .username(savedUser.getUsername()) + .password(savedUser.getPassword()) + .authorities("ROLE_" + savedUser.getRole().name()) + .build(); + + // Generate JWT token for automatic login + String token = jwtUtil.generateToken(userDetails); + + // Build and return authentication response + return AuthResponse.builder() + .token(token) + .type("Bearer") + .id(savedUser.getId()) + .username(savedUser.getUsername()) + .email(savedUser.getEmail()) + .role(savedUser.getRole()) + .build(); + } + + @Override + public AuthResponse login(LoginRequest request) { + log.info("Login attempt for user: {}", request.getUsername()); + + try { + // Authenticate user credentials + // Spring Security will compare plain password with BCrypt hash + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); + } catch (AuthenticationException e) { + log.warn("Login failed for user: {} - Invalid credentials", request.getUsername()); + throw new InvalidCredentialsException("Invalid username or password"); } + + // Load user from database (authentication successful) + User user = + userRepository + .findByUsername(request.getUsername()) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + log.info("User {} logged in successfully", user.getUsername()); + + // Create UserDetails for token generation + org.springframework.security.core.userdetails.UserDetails userDetails = + org.springframework.security.core.userdetails.User.builder() + .username(user.getUsername()) + .password(user.getPassword()) + .authorities("ROLE_" + user.getRole().name()) + .build(); + + // Generate JWT token + String token = jwtUtil.generateToken(userDetails); + + // Build and return authentication response + return AuthResponse.builder() + .token(token) + .type("Bearer") + .id(user.getId()) + .username(user.getUsername()) + .email(user.getEmail()) + .role(user.getRole()) + .build(); + } + + @Override + public UserDto getCurrentUser() { + // Get authentication from SecurityContext (set by JWT filter) + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String username = authentication.getName(); + + log.debug("Fetching current user: {}", username); + + // Load user from database + User user = + userRepository + .findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); + + // Convert to DTO (excludes password) + return UserDto.from(user); + } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 7ed98d0..1b3b27d 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,4 +1,6 @@ spring: + config: + import: "optional:configtree:secrets/" application: name: Expense-Tracker-Backend-Java @@ -21,9 +23,9 @@ spring: # default-schema: public # drop-first: false - jwt: - secret: ${JWT_SECRET:file:./secrets/jwt.secret} - expiration: ${JWT_EXPIRATION:86400000} +jwt: + secret: ${jwt.secrets} + expiration: ${JWT_EXPIRATION:86400000} server: port: ${SERVER_PORT:8080} \ No newline at end of file From 104c1c10daabd73339f62e001a800b83823c50d4 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Mon, 9 Feb 2026 09:37:08 +0100 Subject: [PATCH 26/33] fixed issue where JPA wasn't adding timestamps to base entitiy added @EntityListener annotation and @CreatedAt and @lastModified date to baseEntity, this fixes the entity creation flow --- .../ExpenseTrackerBackendJavaApplication.java | 2 + .../controller/TransactionController.java | 5 ++ .../persistence/entity/BaseEntity.java | 6 ++ .../repository/TransactionRepository.java | 14 +++- .../service/TransactionServiceImpl.java | 72 +++++++++++-------- 5 files changed, 67 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/SkillsForge/expensetracker/ExpenseTrackerBackendJavaApplication.java b/src/main/java/com/SkillsForge/expensetracker/ExpenseTrackerBackendJavaApplication.java index 7f7ff62..7748646 100644 --- a/src/main/java/com/SkillsForge/expensetracker/ExpenseTrackerBackendJavaApplication.java +++ b/src/main/java/com/SkillsForge/expensetracker/ExpenseTrackerBackendJavaApplication.java @@ -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) { diff --git a/src/main/java/com/SkillsForge/expensetracker/controller/TransactionController.java b/src/main/java/com/SkillsForge/expensetracker/controller/TransactionController.java index 09358b2..189dc22 100644 --- a/src/main/java/com/SkillsForge/expensetracker/controller/TransactionController.java +++ b/src/main/java/com/SkillsForge/expensetracker/controller/TransactionController.java @@ -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.*; @@ -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 getAllTransactions( @ModelAttribute TransactionFilter filter, Pageable pageable) { return transactionService.getAllTransactions(filter, pageable); diff --git a/src/main/java/com/SkillsForge/expensetracker/persistence/entity/BaseEntity.java b/src/main/java/com/SkillsForge/expensetracker/persistence/entity/BaseEntity.java index cb59686..0d2a22c 100644 --- a/src/main/java/com/SkillsForge/expensetracker/persistence/entity/BaseEntity.java +++ b/src/main/java/com/SkillsForge/expensetracker/persistence/entity/BaseEntity.java @@ -5,6 +5,9 @@ import java.util.Objects; import lombok.*; import lombok.experimental.SuperBuilder; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; @MappedSuperclass @Getter @@ -13,14 +16,17 @@ @RequiredArgsConstructor @SuperBuilder @AllArgsConstructor +@EntityListeners(AuditingEntityListener.class) public class BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) protected Long id; + @CreatedDate @Column(nullable = false) LocalDateTime createdAt; + @LastModifiedDate @Column(nullable = false) LocalDateTime updatedAt; diff --git a/src/main/java/com/SkillsForge/expensetracker/persistence/repository/TransactionRepository.java b/src/main/java/com/SkillsForge/expensetracker/persistence/repository/TransactionRepository.java index 162c69d..56995a9 100644 --- a/src/main/java/com/SkillsForge/expensetracker/persistence/repository/TransactionRepository.java +++ b/src/main/java/com/SkillsForge/expensetracker/persistence/repository/TransactionRepository.java @@ -1,8 +1,20 @@ package com.SkillsForge.expensetracker.persistence.repository; import com.SkillsForge.expensetracker.persistence.entity.Transaction; +import com.SkillsForge.expensetracker.persistence.entity.User; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; public interface TransactionRepository - extends JpaRepository, JpaSpecificationExecutor {} + extends JpaRepository, JpaSpecificationExecutor { + + Page findAllByUser(User user, Pageable pageable); + + Page findAll(Specification spec, Pageable pageable); + + Optional findByIdAndUser(Long id, User user); +} diff --git a/src/main/java/com/SkillsForge/expensetracker/service/TransactionServiceImpl.java b/src/main/java/com/SkillsForge/expensetracker/service/TransactionServiceImpl.java index 25cf00e..0cc9e5a 100644 --- a/src/main/java/com/SkillsForge/expensetracker/service/TransactionServiceImpl.java +++ b/src/main/java/com/SkillsForge/expensetracker/service/TransactionServiceImpl.java @@ -7,6 +7,8 @@ import com.SkillsForge.expensetracker.exception.ResourceNotFoundException; import com.SkillsForge.expensetracker.persistence.entity.Transaction; import com.SkillsForge.expensetracker.persistence.repository.TransactionRepository; +import com.SkillsForge.expensetracker.persistence.entity.User; +import com.SkillsForge.expensetracker.persistence.repository.UserRepository; import jakarta.persistence.criteria.Predicate; import java.time.LocalDateTime; import java.util.ArrayList; @@ -16,6 +18,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,48 +28,46 @@ public class TransactionServiceImpl implements TransactionService { private final TransactionRepository transactionRepository; + private final UserRepository userRepository; - // ================= CREATE ================= @Override @Transactional public TransactionDto createTransaction(CreateTransactionRequest request) { - LocalDateTime now = LocalDateTime.now(); + User user = getCurrentUser(); Transaction transaction = new Transaction(request); - transaction.setCreatedAt(now); - transaction.setUpdatedAt(now); + transaction.setUser(user); + transaction.setCreatedAt(LocalDateTime.now()); + transaction.setUpdatedAt(LocalDateTime.now()); Transaction saved = transactionRepository.save(transaction); - log.info("Transaction created successfully with ID: {}", saved.getId()); + log.info("Transaction created with ID: {}", saved.getId()); return TransactionDto.fromEntity(saved); } - // ================= GET BY ID ================= @Override @Transactional(readOnly = true) public TransactionDto getTransactionById(Long id) { - log.info("Fetching transaction with ID: {}", id); + User user = getCurrentUser(); - Transaction transaction = - transactionRepository - .findById(id) - .orElseThrow( - () -> new ResourceNotFoundException("Transaction not found with ID: " + id)); + Transaction transaction = transactionRepository + .findByIdAndUser(id, user) + .orElseThrow( + () -> new ResourceNotFoundException("Transaction not found with ID: " + id)); return TransactionDto.fromEntity(transaction); } - // ================= UPDATE ================= @Override @Transactional public TransactionDto updateTransaction(Long id, TransactionUpdateRequest request) { + User user = getCurrentUser(); - Transaction existing = - transactionRepository - .findById(id) - .orElseThrow( - () -> new ResourceNotFoundException("Transaction not found with ID: " + id)); + Transaction existing = transactionRepository + .findByIdAndUser(id, user) + .orElseThrow( + () -> new ResourceNotFoundException("Transaction not found with ID: " + id)); existing.setDescription(request.getDescription()); existing.setCategory(request.getCategory()); @@ -76,31 +77,40 @@ public TransactionDto updateTransaction(Long id, TransactionUpdateRequest reques existing.setUpdatedAt(LocalDateTime.now()); Transaction saved = transactionRepository.save(existing); - log.info("Transaction updated successfully with ID: {}", saved.getId()); + log.info("Transaction updated with ID: {}", saved.getId()); return TransactionDto.fromEntity(saved); } - // ================= GET ALL (FILTERED) ================= @Override @Transactional(readOnly = true) public Page getAllTransactions(TransactionFilter filter, Pageable pageable) { + User user = getCurrentUser(); - Specification spec = - (root, query, cb) -> { - List predicates = new ArrayList<>(); + Specification spec = (root, query, cb) -> { + List predicates = new ArrayList<>(); - if (filter.getCategory() != null) { - predicates.add(cb.equal(root.get("category"), filter.getCategory())); - } + // Force filter by current user + predicates.add(cb.equal(root.get("user"), user)); - if (filter.getType() != null) { - predicates.add(cb.equal(root.get("type"), filter.getType())); - } + if (filter.getCategory() != null) { + predicates.add(cb.equal(root.get("category"), filter.getCategory())); + } - return cb.and(predicates.toArray(new Predicate[0])); - }; + if (filter.getType() != null) { + predicates.add(cb.equal(root.get("type"), filter.getType())); + } + + return cb.and(predicates.toArray(new Predicate[0])); + }; return transactionRepository.findAll(spec, pageable).map(TransactionDto::fromEntity); } + + private User getCurrentUser() { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + return userRepository + .findByUsername(username) + .orElseThrow(() -> new ResourceNotFoundException("User not found")); + } } From 175c129d0bd566e01cd98cb78bce3b0b985d8bed Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Mon, 9 Feb 2026 09:52:08 +0100 Subject: [PATCH 27/33] modified Filter Chain to change endpoint access --- .../expensetracker/app/Config.java | 43 ++++--------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/SkillsForge/expensetracker/app/Config.java b/src/main/java/com/SkillsForge/expensetracker/app/Config.java index 90b861c..b42bacd 100644 --- a/src/main/java/com/SkillsForge/expensetracker/app/Config.java +++ b/src/main/java/com/SkillsForge/expensetracker/app/Config.java @@ -16,9 +16,6 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -/** - * Spring Security Configuration Configures authentication, authorization, and JWT token validation - */ @Configuration @EnableWebSecurity @EnableMethodSecurity // Enables @PreAuthorize, @PostAuthorize annotations @@ -27,14 +24,6 @@ public class Config { private final JwtAuthenticationFilter jwtAuthenticationFilter; - /** - * Configure the security filter chain Defines which endpoints are public and which require - * authentication - * - * @param http HttpSecurity to configure - * @return configured SecurityFilterChain - * @throws Exception if configuration fails - */ @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http @@ -43,17 +32,16 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .csrf(AbstractHttpConfigurer::disable) // Configure endpoint authorization - .authorizeHttpRequests( - auth -> - auth - // Public endpoints - anyone can access - .requestMatchers("/api/v1/auth/**") - .permitAll() - - // All other endpoints require authentication - .anyRequest() - .authenticated()) + .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( @@ -66,24 +54,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http.build(); } - /** - * Password encoder bean for hashing passwords BCrypt is a strong hashing algorithm recommended - * for passwords - * - * @return BCryptPasswordEncoder instance - */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - /** - * Authentication manager bean Used for authenticating users during login - * - * @param config authentication configuration - * @return AuthenticationManager - * @throws Exception if configuration fails - */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { From 850b6b113b3cdfde4e28cab2689252bec38a630c Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Mon, 9 Feb 2026 09:56:53 +0100 Subject: [PATCH 28/33] added loggin to JwtAuthenticationFilter to detect the issue --- .../security/JwtAuthenticationFilter.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/SkillsForge/expensetracker/security/JwtAuthenticationFilter.java b/src/main/java/com/SkillsForge/expensetracker/security/JwtAuthenticationFilter.java index 69e6a4e..275a81b 100644 --- a/src/main/java/com/SkillsForge/expensetracker/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/SkillsForge/expensetracker/security/JwtAuthenticationFilter.java @@ -10,6 +10,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; @@ -19,7 +20,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; - private final UserDetailsServiceImpl userDetailsService; + private final UserDetailsService userDetailsService; @Override protected void doFilterInternal( @@ -35,15 +36,16 @@ protected void doFilterInternal( String jwt = null; // Check if header exists and starts with "Bearer " - if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { - jwt = authorizationHeader.substring(7); // Remove "Bearer " prefix to get token - try { - username = jwtUtil.extractUsername(jwt); // Extract username from token - } catch (Exception e) { - // Token is invalid or expired - log and continue without authentication - logger.warn("JWT token extraction failed: " + e.getMessage()); + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + jwt = authorizationHeader.substring(7); + try { + username = jwtUtil.extractUsername(jwt); + } catch (Exception e) { + // Print the full error to see if it's a Key or Format issue + logger.error("CRITICAL JWT FAILURE: " + e.getMessage()); + e.printStackTrace(); + } } - } // If we have a username and no authentication exists in SecurityContext if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { From 7dc8f5cd27b4afe165e896d424b19ed6f0acb400 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Mon, 9 Feb 2026 10:04:21 +0100 Subject: [PATCH 29/33] refactored secrets file --- src/main/resources/application.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 1b3b27d..5e68c68 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -24,7 +24,7 @@ spring: # drop-first: false jwt: - secret: ${jwt.secrets} + secret: ${jwt.secret} expiration: ${JWT_EXPIRATION:86400000} server: From a4d1350849702857eb6bc8bda93ecae57cf126ef Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Mon, 9 Feb 2026 10:12:45 +0100 Subject: [PATCH 30/33] fixed issue with system_admin credentials --- .../resources/db/changelog/003_add_user_to_transaction.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/db/changelog/003_add_user_to_transaction.xml b/src/main/resources/db/changelog/003_add_user_to_transaction.xml index c219908..1c817c9 100644 --- a/src/main/resources/db/changelog/003_add_user_to_transaction.xml +++ b/src/main/resources/db/changelog/003_add_user_to_transaction.xml @@ -31,8 +31,8 @@ - - + + From 42afa9add5f19cb843bb32134b4fc764ce046694 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Tue, 10 Feb 2026 13:11:37 +0100 Subject: [PATCH 31/33] removed unneeded config in application.yaml --- src/main/resources/application.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 5e68c68..9bb9f3c 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,6 +1,4 @@ spring: - config: - import: "optional:configtree:secrets/" application: name: Expense-Tracker-Backend-Java From 190d21f817f3cc56199f4a46c73e4547419ffde0 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Tue, 10 Feb 2026 13:15:13 +0100 Subject: [PATCH 32/33] added preAuthorize annotation to /me endpoint --- .../java/com/SkillsForge/expensetracker/app/enums/Role.java | 5 ----- .../expensetracker/controller/AuthController.java | 3 +++ 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/SkillsForge/expensetracker/app/enums/Role.java b/src/main/java/com/SkillsForge/expensetracker/app/enums/Role.java index 0280e52..cac7883 100644 --- a/src/main/java/com/SkillsForge/expensetracker/app/enums/Role.java +++ b/src/main/java/com/SkillsForge/expensetracker/app/enums/Role.java @@ -6,11 +6,6 @@ public enum Role { USER, ADMIN; - /** - * Get the permissions associated with this role - * - * @return set of permissions for this role - */ public Set getPermissions() { return switch (this) { case ADMIN -> Set.of(Permissions.values()); // Admin has all permissions diff --git a/src/main/java/com/SkillsForge/expensetracker/controller/AuthController.java b/src/main/java/com/SkillsForge/expensetracker/controller/AuthController.java index be61f62..41466d0 100644 --- a/src/main/java/com/SkillsForge/expensetracker/controller/AuthController.java +++ b/src/main/java/com/SkillsForge/expensetracker/controller/AuthController.java @@ -9,6 +9,7 @@ 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; @@ -34,7 +35,9 @@ public ResponseEntity login(@Valid @RequestBody LoginRequest reque return ResponseEntity.ok(response); } + @GetMapping("/me") + @PreAuthorize("hasAuthority('USER_READ')") public ResponseEntity getCurrentUser() { UserDto user = userService.getCurrentUser(); return ResponseEntity.ok(user); From 074a6fab073167a1909eb488824b65ed105c8ec9 Mon Sep 17 00:00:00 2001 From: Semilore317 Date: Tue, 10 Feb 2026 13:26:34 +0100 Subject: [PATCH 33/33] added JWT_SECRET to IDEA environment variables --- src/main/resources/application.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 9bb9f3c..f26d5aa 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -22,7 +22,7 @@ spring: # drop-first: false jwt: - secret: ${jwt.secret} + secret: ${JWT_SECRET} expiration: ${JWT_EXPIRATION:86400000} server: