Skip to content

Commit ef66bf3

Browse files
committed
Updated to Java 23 and idiomatic Spring
1 parent bb2f2d4 commit ef66bf3

File tree

11 files changed

+203
-133
lines changed

11 files changed

+203
-133
lines changed

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ RUN npm run build
1111
############################
1212
# Java build stage
1313
############################
14-
FROM maven:3.9-eclipse-temurin-17 AS build
14+
FROM maven:3.9-eclipse-temurin-23 AS build
1515
WORKDIR /app
1616
COPY backend/pom.xml backend/pom.xml
1717
RUN --mount=type=cache,target=/root/.m2 mvn -q -DskipTests -f backend/pom.xml dependency:go-offline
@@ -21,7 +21,7 @@ RUN --mount=type=cache,target=/root/.m2 mvn -q -DskipTests -f backend/pom.xml pa
2121
############################
2222
# Runtime image
2323
############################
24-
FROM eclipse-temurin:17-jre
24+
FROM eclipse-temurin:23-jre
2525
WORKDIR /app
2626
# Copy the Spring Boot fat jar (exclude the .original)
2727
COPY --from=build /app/backend/target/*-SNAPSHOT.jar /app/app.jar

backend/pom.xml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<description>Java translation of learn_pastebin-python backend</description>
1010

1111
<properties>
12-
<java.version>17</java.version>
12+
<java.version>23</java.version>
1313
<spring-boot.version>3.3.3</spring-boot.version>
1414
<hibernate.version>6.5.2.Final</hibernate.version>
1515
</properties>
@@ -86,6 +86,18 @@
8686
<target>${java.version}</target>
8787
</configuration>
8888
</plugin>
89+
<plugin>
90+
<groupId>org.apache.maven.plugins</groupId>
91+
<artifactId>maven-jar-plugin</artifactId>
92+
<version>3.3.0</version>
93+
<configuration>
94+
<archive>
95+
<manifestEntries>
96+
<Automatic-Module-Name>com.codesignal.pastebin</Automatic-Module-Name>
97+
</manifestEntries>
98+
</archive>
99+
</configuration>
100+
</plugin>
89101
</plugins>
90102
</build>
91103
</project>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.codesignal.pastebin.config;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
6+
import org.springframework.security.crypto.password.PasswordEncoder;
7+
8+
@Configuration
9+
public class AppConfig {
10+
@Bean
11+
public PasswordEncoder passwordEncoder() {
12+
return new BCryptPasswordEncoder();
13+
}
14+
}

backend/src/main/java/com/codesignal/pastebin/config/CorsConfig.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,26 @@
33
import org.springframework.context.annotation.Bean;
44
import org.springframework.context.annotation.Configuration;
55
import org.springframework.web.cors.CorsConfiguration;
6+
import org.springframework.web.cors.CorsConfigurationSource;
67
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
78
import org.springframework.web.filter.CorsFilter;
89

910
@Configuration
1011
public class CorsConfig {
1112
@Bean
12-
public CorsFilter corsFilter() {
13+
public CorsConfigurationSource corsConfigurationSource() {
1314
CorsConfiguration config = new CorsConfiguration();
1415
config.addAllowedOriginPattern("*");
1516
config.addAllowedHeader("*");
1617
config.addAllowedMethod("*");
1718
config.setAllowCredentials(true);
1819
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
1920
source.registerCorsConfiguration("/**", config);
20-
return new CorsFilter(source);
21+
return source;
22+
}
23+
24+
@Bean
25+
public CorsFilter corsFilter() {
26+
return new CorsFilter(corsConfigurationSource());
2127
}
2228
}

backend/src/main/java/com/codesignal/pastebin/controller/AdminController.java

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,25 @@
44
import com.codesignal.pastebin.model.Role;
55
import com.codesignal.pastebin.model.User;
66
import com.codesignal.pastebin.repo.UserRepository;
7+
import com.codesignal.pastebin.util.ErrorResponse;
78
import com.codesignal.pastebin.util.JwtUtil;
8-
import jakarta.servlet.http.HttpServletResponse;
99
import org.springframework.http.HttpStatus;
1010
import org.springframework.http.ResponseEntity;
11-
import org.springframework.web.bind.annotation.*;
11+
import org.springframework.web.bind.annotation.GetMapping;
12+
import org.springframework.web.bind.annotation.RequestHeader;
13+
import org.springframework.web.bind.annotation.RequestMapping;
14+
import org.springframework.web.bind.annotation.RequestParam;
15+
import org.springframework.web.bind.annotation.RestController;
1216

1317
import javax.sql.DataSource;
1418
import java.sql.Connection;
1519
import java.sql.ResultSet;
1620
import java.sql.ResultSetMetaData;
1721
import java.sql.Statement;
18-
import java.util.*;
22+
import java.util.ArrayList;
23+
import java.util.LinkedHashMap;
24+
import java.util.List;
25+
import java.util.Map;
1926

2027
@RestController
2128
@RequestMapping("/api/admin")
@@ -30,38 +37,36 @@ public AdminController(UserRepository users, JwtUtil jwt, DataSource dataSource)
3037
this.dataSource = dataSource;
3138
}
3239

33-
private ResponseEntity<Map<String, Object>> error(HttpStatus status, String detail) {
34-
Map<String, Object> err = new HashMap<>();
35-
err.put("detail", detail);
36-
return ResponseEntity.status(status).body(err);
40+
private ResponseEntity<ErrorResponse> error(HttpStatus status, String detail) {
41+
return ResponseEntity.status(status).body(new ErrorResponse(detail));
3742
}
3843

39-
private Object[] verifyAdminOrError(String authorizationHeader) {
44+
private AdminOutcome verifyAdminOrError(String authorizationHeader) {
4045
if (authorizationHeader == null || authorizationHeader.isBlank()) {
41-
return new Object[]{null, error(HttpStatus.UNAUTHORIZED, "Missing authorization header")};
46+
return new AdminOutcome(null, error(HttpStatus.UNAUTHORIZED, "Missing authorization header"));
4247
}
4348
String token = authorizationHeader.startsWith("Bearer ") ? authorizationHeader.substring(7) : authorizationHeader;
4449
try {
4550
DecodedJWT decoded = jwt.verify(token);
4651
Integer userId = decoded.getClaim("userId").asInt();
4752
var userOpt = users.findById(userId);
4853
if (userOpt.isEmpty()) {
49-
return new Object[]{null, error(HttpStatus.UNAUTHORIZED, "User not found")};
54+
return new AdminOutcome(null, error(HttpStatus.UNAUTHORIZED, "User not found"));
5055
}
5156
User u = userOpt.get();
5257
if (u.getRole() != Role.ADMIN) {
53-
return new Object[]{null, error(HttpStatus.FORBIDDEN, "Access denied")};
58+
return new AdminOutcome(null, error(HttpStatus.FORBIDDEN, "Access denied"));
5459
}
55-
return new Object[]{u, null};
60+
return new AdminOutcome(u, null);
5661
} catch (Exception e) {
57-
return new Object[]{null, error(HttpStatus.UNAUTHORIZED, "Invalid token")};
62+
return new AdminOutcome(null, error(HttpStatus.UNAUTHORIZED, "Invalid token"));
5863
}
5964
}
6065

6166
@GetMapping("/test")
6267
public ResponseEntity<?> adminTest(@RequestHeader(value = "authorization", required = false) String authorization) {
63-
Object[] res = verifyAdminOrError(authorization);
64-
if (res[1] != null) return (ResponseEntity<?>) res[1];
68+
var outcome = verifyAdminOrError(authorization);
69+
if (outcome.error() != null) return outcome.error();
6570
return ResponseEntity.ok(Map.of("message", "Admin test endpoint accessed successfully"));
6671
}
6772

@@ -76,7 +81,6 @@ public ResponseEntity<?> accountInfo(@RequestParam(name = "id", required = false
7681
return error(HttpStatus.BAD_REQUEST, "Missing user ID parameter");
7782
}
7883

79-
// Deliberately vulnerable: Direct string concatenation into SQL
8084
String sql = "SELECT * FROM users WHERE id = " + id;
8185
try (Connection conn = dataSource.getConnection();
8286
Statement st = conn.createStatement();
@@ -96,4 +100,7 @@ public ResponseEntity<?> accountInfo(@RequestParam(name = "id", required = false
96100
return error(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error");
97101
}
98102
}
103+
104+
private record AdminOutcome(User user, ResponseEntity<ErrorResponse> error) {
105+
}
99106
}

backend/src/main/java/com/codesignal/pastebin/controller/AuthController.java

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,34 @@
33
import com.codesignal.pastebin.model.Role;
44
import com.codesignal.pastebin.model.User;
55
import com.codesignal.pastebin.repo.UserRepository;
6+
import com.codesignal.pastebin.util.ErrorResponse;
67
import com.codesignal.pastebin.util.JwtUtil;
78
import org.springframework.http.HttpStatus;
89
import org.springframework.http.ResponseEntity;
9-
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
10-
import org.springframework.web.bind.annotation.*;
11-
12-
import java.util.HashMap;
13-
import java.util.Map;
10+
import org.springframework.security.crypto.password.PasswordEncoder;
11+
import org.springframework.web.bind.annotation.PostMapping;
12+
import org.springframework.web.bind.annotation.RequestBody;
13+
import org.springframework.web.bind.annotation.RequestMapping;
14+
import org.springframework.web.bind.annotation.RestController;
1415

1516
@RestController
1617
@RequestMapping("/api/auth")
1718
public class AuthController {
1819
private final UserRepository users;
1920
private final JwtUtil jwt;
20-
private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
21+
private final PasswordEncoder encoder;
2122

22-
public AuthController(UserRepository users, JwtUtil jwt) {
23+
public AuthController(UserRepository users, JwtUtil jwt, PasswordEncoder encoder) {
2324
this.users = users;
2425
this.jwt = jwt;
26+
this.encoder = encoder;
2527
}
2628

2729
@PostMapping("/register")
28-
public ResponseEntity<?> register(@RequestBody Map<String, Object> body) {
29-
String username = (String) body.get("username");
30-
String password = (String) body.get("password");
31-
String roleRaw = (String) body.getOrDefault("role", "user");
30+
public ResponseEntity<?> register(@RequestBody RegisterRequest request) {
31+
String username = request.username();
32+
String password = request.password();
33+
String roleRaw = request.role() == null ? "user" : request.role();
3234

3335
if (username == null || password == null) {
3436
return error(HttpStatus.BAD_REQUEST, "Username and password are required");
@@ -38,41 +40,46 @@ public ResponseEntity<?> register(@RequestBody Map<String, Object> body) {
3840
return error(HttpStatus.BAD_REQUEST, "Username already exists");
3941
}
4042

41-
Role role = "admin".equals(roleRaw) ? Role.ADMIN : Role.USER;
43+
Role role = switch (roleRaw) {
44+
case "admin" -> Role.ADMIN;
45+
default -> Role.USER;
46+
};
4247
User u = new User();
4348
u.setUsername(username);
4449
u.setPassword(encoder.encode(password));
4550
u.setRole(role);
4651
users.save(u);
4752

48-
Map<String, Object> res = new HashMap<>();
49-
res.put("message", "User registered successfully");
50-
res.put("userId", u.getId());
51-
return ResponseEntity.ok(res);
53+
return ResponseEntity.ok(new RegisterResponse("User registered successfully", u.getId()));
5254
}
5355

5456
@PostMapping("/login")
55-
public ResponseEntity<?> login(@RequestBody Map<String, Object> body) {
56-
String username = (String) body.get("username");
57-
String password = (String) body.get("password");
58-
59-
var userOpt = users.findByUsername(username);
60-
if (userOpt.isPresent()) {
61-
User u = userOpt.get();
62-
if (encoder.matches(password, u.getPassword())) {
63-
String token = jwt.generateTokenWithUserId(u.getId());
64-
Map<String, Object> res = new HashMap<>();
65-
res.put("token", token);
66-
return ResponseEntity.ok(res);
67-
}
57+
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
58+
String username = request.username();
59+
String password = request.password();
60+
61+
User user = users.findByUsername(username).orElse(null);
62+
if (user == null || !encoder.matches(password, user.getPassword())) {
63+
return error(HttpStatus.UNAUTHORIZED, "Invalid credentials");
6864
}
69-
return error(HttpStatus.UNAUTHORIZED, "Invalid credentials");
65+
66+
String token = jwt.generateTokenWithUserId(user.getId());
67+
return ResponseEntity.ok(new TokenResponse(token));
7068
}
7169

72-
private ResponseEntity<Map<String, Object>> error(HttpStatus status, String detail) {
73-
Map<String, Object> err = new HashMap<>();
74-
err.put("detail", detail);
75-
return ResponseEntity.status(status).body(err);
70+
private ResponseEntity<ErrorResponse> error(HttpStatus status, String detail) {
71+
return ResponseEntity.status(status).body(new ErrorResponse(detail));
72+
}
73+
74+
public record RegisterRequest(String username, String password, String role) {
7675
}
77-
}
7876

77+
public record LoginRequest(String username, String password) {
78+
}
79+
80+
public record RegisterResponse(String message, Integer userId) {
81+
}
82+
83+
public record TokenResponse(String token) {
84+
}
85+
}

0 commit comments

Comments
 (0)