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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions backend/smartjam-api/build.gradle
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation "org.springframework.boot:spring-boot-starter-validation"
implementation 'org.springframework.boot:spring-boot-starter-security'

implementation 'io.jsonwebtoken:jjwt-api:0.13.0'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.13.0'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.13.0'

implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'org.postgresql:postgresql'

runtimeOnly('org.postgresql:postgresql')

implementation 'org.springframework.boot:spring-boot-starter-kafka'
testImplementation 'org.springframework.kafka:spring-kafka-test'


implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.2'


implementation project(':smartjam-common')
implementation("software.amazon.awssdk:s3:2.29.22")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.smartjam.smartjamapi.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@RequiredArgsConstructor
public class ApplicationConfig {

private final CustomUserDetailsService customUserDetailsService;

@Bean
public UserDetailsService userDetailsService() {
return customUserDetailsService::loadUserByUsername;
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.smartjam.smartjamapi.config;

import jakarta.validation.constraints.NotBlank;

import com.smartjam.smartjamapi.entity.UserEntity;
import com.smartjam.smartjamapi.repository.UserRepository;
import com.smartjam.smartjamapi.security.UserDetailsImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

@Override
public UserDetailsImpl loadUserByUsername(@NotBlank String email) throws UsernameNotFoundException {
UserEntity user = userRepository
.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("Invalid credentials"));
Comment on lines +19 to +23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

@NotBlank on method parameter requires Spring method validation to be enabled.

The @NotBlank annotation on email won't enforce validation at runtime unless MethodValidationPostProcessor is configured and the class is annotated with @Validated. Without this, null/blank values will still reach findByEmail. Consider adding an explicit null check for defense-in-depth.

Proposed defensive check
     `@Override`
     public UserDetailsImpl loadUserByUsername(`@NotBlank` String email) throws UsernameNotFoundException {
+        if (email == null || email.isBlank()) {
+            throw new UsernameNotFoundException("Invalid credentials");
+        }
         UserEntity user = userRepository
                 .findByEmail(email)
                 .orElseThrow(() -> new UsernameNotFoundException("Invalid credentials"));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/config/CustomUserDetailsService.java`
around lines 18 - 22, The `@NotBlank` on the loadUserByUsername(String email)
parameter won't be enforced unless method validation is enabled; update
CustomUserDetailsService by either (A) enabling Spring method validation
(register a MethodValidationPostProcessor bean and annotate the class with
`@Validated`) so `@NotBlank` is enforced on UserDetailsService.loadUserByUsername,
or (B) add an explicit defensive check at the start of loadUserByUsername
(validate email != null && !email.isBlank()) and throw a clear
UsernameNotFoundException or IllegalArgumentException before calling
userRepository.findByEmail(...); choose one approach and apply it consistently.


return UserDetailsImpl.build(user);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.smartjam.smartjamapi.config;

import com.smartjam.smartjamapi.security.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthFilter;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth.requestMatchers("/api/auth/**")
.permitAll()
.anyRequest()
.authenticated())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.smartjam.smartjamapi.controller;

import jakarta.validation.Valid;

import com.smartjam.smartjamapi.dto.AuthResponse;
import com.smartjam.smartjamapi.dto.LoginRequest;
import com.smartjam.smartjamapi.dto.RefreshTokenRequest;
import com.smartjam.smartjamapi.dto.RegisterRequest;
import com.smartjam.smartjamapi.service.AuthService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

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

private final AuthService authService;

@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@RequestBody @Valid LoginRequest request) {
log.info("Calling login");
return ResponseEntity.ok(authService.login(request));
}

@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@RequestBody @Valid RegisterRequest request) {
log.info("Calling register");
return ResponseEntity.status(201).body(authService.register(request));
}

@PostMapping("/refresh")
public ResponseEntity<AuthResponse> getNewTokens(@RequestBody @Valid RefreshTokenRequest refreshTokenRequest) {
log.info("Calling getNewToken");
return ResponseEntity.ok(authService.getNewTokens(refreshTokenRequest));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.smartjam.smartjamapi.controller;

import java.security.Principal;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/secured")
@Slf4j
public class MainController {

@GetMapping("/user")
public String userAccess(Principal principal) {
log.info(principal.getName());

log.info("userAccess called for: {}", principal.getName());
return principal.getName();
}

@GetMapping("/hello")
// @PreAuthorize()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Remove commented-out @PreAuthorize (or enable it with a real expression).

Line 24 leaves dead/commented security code in place, which makes intent unclear.

🧹 Proposed cleanup
-    //    `@PreAuthorize`()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// @PreAuthorize()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/controller/MainController.java`
at line 24, Remove the dead commented security annotation by deleting the
commented-out "@PreAuthorize()" line in the MainController class; alternatively,
if security is intended, replace it with a valid expression such as a proper
SpEL like `@PreAuthorize`("hasRole('ROLE_USER')") on the controller class or the
specific handler method (MainController) so the intent is explicit and the code
is not left as commented-out noise.

public String hello() {
return "You are auth";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo: "auth" should likely be "authenticated".

The message "You are auth" appears truncated. Consider using a complete message.

Proposed fix
-        return "You are auth";
+        return "You are authenticated";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return "You are auth";
return "You are authenticated";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/controller/MainController.java`
at line 31, The response string in MainController.java is truncated: replace the
return value "You are auth" with a full, user-friendly phrase like "You are
authenticated" in the method that currently returns that string (locate the
controller method in class MainController that returns the authentication
message) so the endpoint returns a clear, grammatically correct message.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.smartjam.smartjamapi.controller;

import jakarta.validation.Valid;

import com.smartjam.smartjamapi.dto.UploadRequest;
import com.smartjam.smartjamapi.dto.UploadUrlResponse;
import com.smartjam.smartjamapi.service.UploadService;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@AllArgsConstructor
public class UploadController {

private final UploadService uploadService;

@PostMapping("/upload-url")
public ResponseEntity<UploadUrlResponse> getUploadUrl(@RequestBody @Valid UploadRequest request) {
return ResponseEntity.ok(uploadService.generateUploadUrl(request.fileName()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.smartjam.smartjamapi.dto;

import com.fasterxml.jackson.annotation.JsonProperty;

public record AuthResponse(
@JsonProperty("refresh_token") String refreshToken,
@JsonProperty("access_token") String accessToken) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.smartjam.smartjamapi.dto;

import java.time.LocalDateTime;

import com.smartjam.smartjamapi.enums.ErrorCode;
import lombok.Builder;

@Builder
public record ErrorResponseDto(ErrorCode code, String message, LocalDateTime errorTime) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.smartjam.smartjamapi.dto;

public record LoginRequest(String email, String password) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.smartjam.smartjamapi.dto;

import jakarta.validation.constraints.NotBlank;

import com.fasterxml.jackson.annotation.JsonProperty;

public record RefreshTokenRequest(
@NotBlank @JsonProperty("refresh_token") String refreshToken) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.smartjam.smartjamapi.dto;

public record RegisterRequest(String email, String nickname, String password) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.smartjam.smartjamapi.dto;

import jakarta.validation.constraints.NotBlank;

public record UploadRequest(@NotBlank String fileName) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.smartjam.smartjamapi.dto;

public record UploadUrlResponse(String uploadUrl) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.smartjam.smartjamapi.entity;

import java.time.Instant;
import java.util.UUID;

import jakarta.persistence.*;

import com.smartjam.smartjamapi.enums.StatusRefreshToken;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "refresh_tokens")
@Data
@NoArgsConstructor
Comment on lines +14 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

@Data on JPA entities with lazy associations can cause issues.

@Data generates toString(), equals(), and hashCode() that may inadvertently access the lazy-loaded user field, triggering unintended database queries or LazyInitializationException outside a transaction. Consider using @Getter @Setter`` with manually defined equals/`hashCode` based on `id`, or exclude the `user` field from these methods.

Proposed fix
-@Data
+@Getter
+@Setter
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
 `@NoArgsConstructor`
 public class RefreshTokenEntity {
     `@Id`
     `@GeneratedValue`(strategy = GenerationType.UUID)
+    `@EqualsAndHashCode.Include`
     private UUID id;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/RefreshTokenEntity.java`
around lines 14 - 15, The RefreshTokenEntity currently uses Lombok's `@Data` which
generates toString/equals/hashCode that can touch the lazy 'user' association;
replace `@Data` on RefreshTokenEntity with `@Getter` and `@Setter` (and keep
`@NoArgsConstructor`), and implement equals(Object) and hashCode() manually to use
only the primary identifier (id) or otherwise exclude the 'user' field from
those methods; ensure to remove any Lombok-generated toString that includes
'user' (or annotate user to be excluded) so lazy loading won't be triggered
outside a transaction.

public class RefreshTokenEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;

@Column(nullable = false, unique = true)
private String tokenHash;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private UserEntity user;

@Column(nullable = false, name = "expires_at")
private Instant expiresAt;

@Column(nullable = false)
private StatusRefreshToken status;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.smartjam.smartjamapi.entity;

import java.time.Instant;
import java.util.UUID;

import jakarta.persistence.*;

import com.smartjam.smartjamapi.enums.Role;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "users")
@Entity
public class UserEntity {

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

@Column(nullable = false)
private String nickname;

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

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

@Column(name = "first_name")
private String firstName;

@Column(name = "last_name")
private String lastName;

@Column(name = "avatar_url")
private String avatarUrl;

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

@Column(name = "fcm_token")
private String fcmToken;

@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;

@UpdateTimestamp
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;

// @Override
// public @NonNull Collection<? extends GrantedAuthority> getAuthorities() {
// return List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
// }
//
// @Override
// public @NonNull String getPassword() {
// return passwordHash;
// }
}
Comment on lines +61 to +70
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Remove commented-out dead code.

The commented-out UserDetails methods are no longer needed since UserEntity no longer implements the interface. Dead code should be removed to maintain code cleanliness.

🧹 Proposed cleanup
     `@Column`(name = "fcm_token")
     private String fcmToken;
-
-    //    `@Override`
-    //    public `@NonNull` Collection<? extends GrantedAuthority> getAuthorities() {
-    //        return List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
-    //    }
-    //
-    //    `@Override`
-    //    public `@NonNull` String getPassword() {
-    //        return passwordHash;
-    //    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// @Override
// public @NonNull Collection<? extends GrantedAuthority> getAuthorities() {
// return List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
// }
//
// @Override
// public @NonNull String getPassword() {
// return passwordHash;
// }
}
`@Column`(name = "fcm_token")
private String fcmToken;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/entity/UserEntity.java`
around lines 50 - 59, Remove the commented-out dead UserDetails methods from
UserEntity: delete the commented blocks for getAuthorities() and getPassword()
(the lines referencing SimpleGrantedAuthority, ROLE_ + role.name(), and
passwordHash) since UserEntity no longer implements UserDetails; ensure no other
references to those methods remain and run a quick compile to confirm clean
build.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.smartjam.smartjamapi.enums;

public enum AvailabilityStatus {
AVAILABLE,
UNAVAILABLE,
PENDING,
BANNED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.smartjam.smartjamapi.enums;

public enum ErrorCode {
INTERNAL_SERVER_ERROR,
NOT_FOUND,
BAD_REQUEST,
UNAUTHORIZED,
RESOURCE_NOT_FOUND
}
Comment on lines +3 to +9
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider consolidating NOT_FOUND and RESOURCE_NOT_FOUND.

Both constants represent similar "not found" semantics. Having both may cause inconsistency in which one is used across the codebase. Consider using a single constant or clearly documenting when each should be used.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/enums/ErrorCode.java`
around lines 3 - 9, The ErrorCode enum defines both NOT_FOUND and
RESOURCE_NOT_FOUND which are semantically overlapping; pick a single
representative constant (e.g., NOT_FOUND) and remove the duplicate
(RESOURCE_NOT_FOUND) from ErrorCode, then update all usages to reference the
chosen constant (search for ErrorCode.RESOURCE_NOT_FOUND and
ErrorCode.NOT_FOUND) or add a deprecated alias for RESOURCE_NOT_FOUND that maps
to NOT_FOUND to preserve backward compatibility before removing; ensure tests
and any switch/case handling in controllers/services (where ErrorCode is
matched) are updated accordingly.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.smartjam.smartjamapi.enums;

public enum Role {
STUDENT,
TEACHER
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.smartjam.smartjamapi.enums;

public enum StatusRefreshToken {
ACTIVE,
USED,
REVOKED
}
Loading