diff --git a/Dockerfile b/Dockerfile
index 0303ba8..b75d487 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -42,4 +42,4 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
CMD wget -q --spider http://localhost:8080/actuator/health || exit 1
# Run the application
-ENTRYPOINT ["java", "-jar", "app.jar"]
+ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
diff --git a/docker-compose.yml b/docker-compose.yml
index 17d6ae2..c50d729 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -42,9 +42,9 @@ services:
# REVIEWER mode: comma-separated API keys
GEMINI_REVIEWER_KEYS: ${GEMINI_REVIEWER_KEYS:-}
# Grading model fallback chain (PROD + REVIEWER)
- GEMINI_GRADING_MODELS: ${GEMINI_GRADING_MODELS:-gemini-3-flash-preview,gemini-2.5-flash,gemini-2.5-flash-lite,gemma-3-12b-it}
+ GEMINI_GRADING_MODELS: ${GEMINI_GRADING_MODELS:-gemini-2.5-flash,gemini-2.5-flash-lite,gemma-3-12b-it}
# JVM options
- JAVA_OPTS: ${JAVA_OPTS:-"-Xmx512m -Xms256m"}
+ JAVA_OPTS: ${JAVA_OPTS:--Xmx512m -Xms256m}
depends_on:
postgres:
condition: service_healthy
diff --git a/pom.xml b/pom.xml
index 344a79b..ffebdee 100644
--- a/pom.xml
+++ b/pom.xml
@@ -84,27 +84,7 @@
org.springframework.boot
- spring-boot-starter-data-jpa-test
- test
-
-
- org.springframework.boot
- spring-boot-starter-flyway-test
- test
-
-
- org.springframework.boot
- spring-boot-starter-thymeleaf-test
- test
-
-
- org.springframework.boot
- spring-boot-starter-validation-test
- test
-
-
- org.springframework.boot
- spring-boot-starter-webmvc-test
+ spring-boot-starter-test
test
@@ -129,6 +109,10 @@
1.20.4
test
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
com.squareup.okhttp3
diff --git a/src/main/java/net/k2ai/interviewSimulator/config/GeminiConfig.java b/src/main/java/net/k2ai/interviewSimulator/config/GeminiConfig.java
index 415d4cb..b32c270 100644
--- a/src/main/java/net/k2ai/interviewSimulator/config/GeminiConfig.java
+++ b/src/main/java/net/k2ai/interviewSimulator/config/GeminiConfig.java
@@ -18,6 +18,10 @@
@ConfigurationProperties(prefix = "gemini")
public class GeminiConfig {
+ /**
+ * Application mode: DEV, PROD, or REVIEWER.
+ * Note: Uses @Value because app.mode is outside the "gemini" prefix.
+ */
@Value("${app.mode:DEV}")
private String appMode;
@@ -25,7 +29,7 @@ public class GeminiConfig {
private String liveModel = "gemini-2.0-flash-exp";
- private String gradingModel = "gemini-2.5-pro-preview-05-06";
+ private String gradingModel = "gemini-2.5-flash";
private String voiceName = "Aoede";
diff --git a/src/main/java/net/k2ai/interviewSimulator/config/SecurityConfig.java b/src/main/java/net/k2ai/interviewSimulator/config/SecurityConfig.java
index 8230f34..24ee8ca 100644
--- a/src/main/java/net/k2ai/interviewSimulator/config/SecurityConfig.java
+++ b/src/main/java/net/k2ai/interviewSimulator/config/SecurityConfig.java
@@ -7,6 +7,7 @@
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter;
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;
@@ -38,6 +39,11 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.csrf(csrf -> csrf
.ignoringRequestMatchers("/ws/**", "/api/**")
)
+ .sessionManagement(session -> session
+ .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
+ .sessionFixation().migrateSession()
+ .maximumSessions(1)
+ )
.headers(headers -> headers
.frameOptions(HeadersConfigurer.FrameOptionsConfig::deny)
.xssProtection(xss -> xss
@@ -55,7 +61,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.contentSecurityPolicy(csp -> csp
.policyDirectives(
"default-src 'self'; " +
- "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.tailwindcss.com https://cdn.jsdelivr.net; " +
+ "script-src 'self' https://cdn.tailwindcss.com https://cdn.jsdelivr.net; " +
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdnjs.cloudflare.com; " +
"font-src 'self' https://fonts.gstatic.com https://cdnjs.cloudflare.com; " +
"img-src 'self' data:; " +
diff --git a/src/main/java/net/k2ai/interviewSimulator/config/WebSocketConfig.java b/src/main/java/net/k2ai/interviewSimulator/config/WebSocketConfig.java
index 9192b2a..24f8302 100644
--- a/src/main/java/net/k2ai/interviewSimulator/config/WebSocketConfig.java
+++ b/src/main/java/net/k2ai/interviewSimulator/config/WebSocketConfig.java
@@ -25,8 +25,14 @@ public void configureMessageBroker(MessageBrokerRegistry config) {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// WebSocket endpoint that browser connects to
+ // In production, restrict to your domain via ALLOWED_ORIGINS env var
+ String allowedOrigins = System.getenv("ALLOWED_ORIGINS");
+ String[] origins = (allowedOrigins != null && !allowedOrigins.isBlank())
+ ? allowedOrigins.split(",")
+ : new String[]{"http://localhost:8080", "http://127.0.0.1:8080"};
+
registry.addEndpoint("/ws/interview")
- .setAllowedOriginPatterns("*")
+ .setAllowedOriginPatterns(origins)
.withSockJS();
}//registerStompEndpoints
diff --git a/src/main/java/net/k2ai/interviewSimulator/controller/AdminController.java b/src/main/java/net/k2ai/interviewSimulator/controller/AdminController.java
index e58c328..7c5ebe1 100644
--- a/src/main/java/net/k2ai/interviewSimulator/controller/AdminController.java
+++ b/src/main/java/net/k2ai/interviewSimulator/controller/AdminController.java
@@ -79,10 +79,9 @@ public String dashboard(
// Override totalSessions with actual total from pagination (respects filters)
stats.put("totalSessions", pageData.get("totalElements"));
- // Build feedback lookup map (sessionId -> feedback)
- Map feedbackMap = sessions.stream()
- .map(s -> feedbackRepository.findBySessionId(s.getId()).orElse(null))
- .filter(f -> f != null)
+ // Build feedback lookup map (sessionId -> feedback) — single batch query
+ List sessionIds = sessions.stream().map(InterviewSession::getId).toList();
+ Map feedbackMap = feedbackRepository.findBySessionIdIn(sessionIds).stream()
.collect(Collectors.toMap(f -> f.getSession().getId(), f -> f));
// Calculate durations (in minutes)
diff --git a/src/main/java/net/k2ai/interviewSimulator/controller/ApiKeyController.java b/src/main/java/net/k2ai/interviewSimulator/controller/ApiKeyController.java
index 26293fb..6d7cf51 100644
--- a/src/main/java/net/k2ai/interviewSimulator/controller/ApiKeyController.java
+++ b/src/main/java/net/k2ai/interviewSimulator/controller/ApiKeyController.java
@@ -130,11 +130,8 @@ public ResponseEntity