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
10 changes: 5 additions & 5 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: zulu
java-version: '11'
- uses: actions/cache@v2
java-version: '17'
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ It uses a ~~H2 in-memory database~~ sqlite database (for easy local test without

# Getting started

You'll need Java 11 installed.
You'll need Java 17 installed.

./gradlew bootRun

Expand Down
39 changes: 20 additions & 19 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
plugins {
id 'org.springframework.boot' version '2.6.3'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'org.springframework.boot' version '3.2.5'
id 'io.spring.dependency-management' version '1.1.4'
id 'java'
id "com.netflix.dgs.codegen" version "5.0.6"
id "com.diffplug.spotless" version "6.2.1"
id "com.netflix.dgs.codegen" version "6.0.3"
id "com.diffplug.spotless" version "6.25.0"
}

version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
targetCompatibility = '11'
sourceCompatibility = '17'
targetCompatibility = '17'

spotless {
java {
target project.fileTree(project.rootDir) {
include '**/*.java'
exclude 'build/generated/**/*.*', 'build/generated-examples/**/*.*'
exclude 'build/**', 'frontend/**'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Spotless target now skips all Java under build/ and frontend/

The exclude was broadened from the two generated-source directories to build/** and frontend/**. frontend/ currently contains no Java files, so the only practical effect is that DGS-generated sources under build/generated* are no longer formatted (as before) plus any future build outputs — no behavioral risk, but the frontend/** exclusion is currently a no-op.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Right — frontend/** is defensive only. build/** is the load-bearing part: under Gradle 8 scanning build/ made spotlessJava consume processResources output and fail the implicit-dependency check.

}
googleJavaFormat()
}
Expand All @@ -35,25 +35,25 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-hateoas'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2'
implementation 'com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter:4.9.21'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
implementation 'com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter:8.5.8'
implementation 'org.flywaydb:flyway-core'
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2',
'io.jsonwebtoken:jjwt-jackson:0.11.2'
implementation 'joda-time:joda-time:2.10.13'
implementation 'org.xerial:sqlite-jdbc:3.36.0.3'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5',
'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'joda-time:joda-time:2.12.7'
implementation 'org.xerial:sqlite-jdbc:3.45.3.0'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

testImplementation 'io.rest-assured:rest-assured:4.5.1'
testImplementation 'io.rest-assured:json-path:4.5.1'
testImplementation 'io.rest-assured:xml-path:4.5.1'
testImplementation 'io.rest-assured:spring-mock-mvc:4.5.1'
testImplementation 'io.rest-assured:rest-assured:5.4.0'
testImplementation 'io.rest-assured:json-path:5.4.0'
testImplementation 'io.rest-assured:xml-path:5.4.0'
testImplementation 'io.rest-assured:spring-mock-mvc:5.4.0'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:2.2.2'
testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.3'
}

tasks.named('test') {
Expand All @@ -69,4 +69,5 @@ tasks.named('clean') {
tasks.named('generateJava') {
schemaPaths = ["${projectDir}/src/main/resources/schema"] // List of directories containing schema files
packageName = 'io.spring.graphql' // The package name to use to generate sources
typeMapping = ["PageInfo": "graphql.relay.PageInfo"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 PageInfo type mapping relies on graphql.relay.PageInfo field coercion

Mapping the schema PageInfo to graphql.relay.PageInfo keeps the datafetchers compiling, but the schema declares endCursor/startCursor as String while graphql.relay.PageInfo exposes them as ConnectionCursor. Serialization works only because graphql-java's String coercing falls back to String.valueOf(...). This was the pre-upgrade behavior too, but it is worth verifying against the newer graphql-java version bundled with DGS 8, since stricter scalar coercion would surface as runtime serialization errors on paginated GraphQL queries.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verified against the running app on the upgraded stack — Relay coercion still works with the graphql-java bundled by DGS 8.5.8:

{ articles(first:5) { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } edges { cursor node { slug } } } }
-> {"pageInfo":{"hasNextPage":false,"hasPreviousPage":false,"startCursor":"1786387329360","endCursor":"1786386909054"}, ...}

}
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
2 changes: 1 addition & 1 deletion src/main/java/io/spring/api/ArticleApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
import io.spring.core.article.ArticleRepository;
import io.spring.core.service.AuthorizationService;
import io.spring.core.user.User;
import jakarta.validation.Valid;
import java.util.HashMap;
import java.util.Map;
import javax.validation.Valid;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/io/spring/api/ArticlesApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import io.spring.application.article.NewArticleParam;
import io.spring.core.article.Article;
import io.spring.core.user.User;
import jakarta.validation.Valid;
import java.util.HashMap;
import javax.validation.Valid;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/io/spring/api/CommentsApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
import io.spring.core.comment.CommentRepository;
import io.spring.core.service.AuthorizationService;
import io.spring.core.user.User;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/io/spring/api/CurrentUserApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
import io.spring.application.user.UpdateUserParam;
import io.spring.application.user.UserService;
import io.spring.core.user.User;
import jakarta.validation.Valid;
import java.util.HashMap;
import java.util.Map;
import javax.validation.Valid;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/io/spring/api/UsersApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
import io.spring.core.service.JwtService;
import io.spring.core.user.User;
import io.spring.core.user.UserRepository;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import javax.validation.Valid;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY;

import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
Expand Down Expand Up @@ -63,7 +63,7 @@ public ResponseEntity<Object> handleInvalidAuthentication(
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException e,
HttpHeaders headers,
HttpStatus status,
HttpStatusCode status,
WebRequest request) {
List<FieldErrorResource> errorResources =
e.getBindingResult().getFieldErrors().stream()
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/io/spring/api/security/JwtTokenFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

import io.spring.core.service.JwtService;
import io.spring.core.user.UserRepository;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.Optional;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
Expand Down
66 changes: 35 additions & 31 deletions src/main/java/io/spring/api/security/WebSecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package io.spring.api.security;

import static java.util.Arrays.asList;
import static org.springframework.security.config.Customizer.withDefaults;

import jakarta.servlet.DispatcherType;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
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.configuration.WebSecurityConfigurerAdapter;
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.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
Expand All @@ -20,7 +22,7 @@

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public class WebSecurityConfig {

@Bean
public JwtTokenFilter jwtTokenFilter() {
Comment on lines 27 to 28

@devin-ai-integration devin-ai-integration Bot Aug 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: JwtTokenFilter bean is still auto-registered in the servlet chain

jwtTokenFilter() remains a @Bean of type Filter, so Spring Boot's servlet auto-registration adds it to the container filter chain in addition to the security chain (addFilterBefore). This was pre-existing, but under Spring Security 6 the chain is now built exclusively from this bean, making the double registration more visible: the filter will populate the SecurityContext for every request (including static/error dispatches) outside the security chain. Consider returning it as a plain new JwtTokenFilter() inside the chain or registering a FilterRegistrationBean with setEnabled(false).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pre-existing behavior that this upgrade doesn't change, and disabling the auto-registration would alter runtime behavior beyond the scope of the upgrade, so leaving it as-is. Worth a follow-up if the maintainers want the filter confined to the security chain.

Expand All @@ -32,36 +34,38 @@ public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Override
protected void configure(HttpSecurity http) throws Exception {

http.csrf()
.disable()
.cors()
.and()
.exceptionHandling()
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS)
.permitAll()
.antMatchers("/graphiql")
.permitAll()
.antMatchers("/graphql")
.permitAll()
.antMatchers(HttpMethod.GET, "/articles/feed")
.authenticated()
.antMatchers(HttpMethod.POST, "/users", "/users/login")
.permitAll()
.antMatchers(HttpMethod.GET, "/articles/**", "/profiles/**", "/tags")
.permitAll()
.anyRequest()
.authenticated();
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.cors(withDefaults())
.exceptionHandling(
exceptions ->
exceptions.authenticationEntryPoint(
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)))
.sessionManagement(
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(
requests ->
requests
.dispatcherTypeMatchers(DispatcherType.ERROR)
.permitAll()
Comment on lines +50 to +51

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Permitting ERROR dispatch bypasses authorization for error responses

dispatcherTypeMatchers(DispatcherType.ERROR).permitAll() is required under Spring Security 6 (the security filter is registered for REQUEST, ASYNC and ERROR dispatch, and authorizeHttpRequests would otherwise re-authorize the /error forward and turn 404/422 into 401). Worth confirming that no sensitive content is rendered through the error dispatch path, since any error-dispatched request now skips authorization entirely; with the default BasicErrorController body this is fine.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Confirmed: the app has no custom error controller or /error view, so the ERROR dispatch renders the default BasicErrorController body only.

.requestMatchers(HttpMethod.OPTIONS, "/**")
.permitAll()
.requestMatchers("/graphiql", "/graphiql/**")
.permitAll()
.requestMatchers("/graphql")
.permitAll()
.requestMatchers(HttpMethod.GET, "/articles/feed")
.authenticated()
.requestMatchers(HttpMethod.POST, "/users", "/users/login")
.permitAll()
.requestMatchers(HttpMethod.GET, "/articles/**", "/profiles/**", "/tags")
.permitAll()
Comment on lines +52 to +63

@devin-ai-integration devin-ai-integration Bot Aug 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: requestMatchers now resolve as MvcRequestMatcher instead of Ant matchers

Replacing antMatchers with requestMatchers changes matcher resolution: when Spring MVC is on the classpath the rules become MvcRequestMatchers evaluated against the path within the DispatcherServlet mapping. For paths served by static resource handling or by DGS/spring-graphql outside DispatcherServlet mappings (e.g. /graphiql assets), matching semantics can differ subtly from Ant matching. The added /graphiql/** rule covers the documented case, but if additional non-DispatcherServlet endpoints (actuator, static files) are added later they may not match as expected.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

.anyRequest()
.authenticated())
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment on lines +52 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Security rule ordering and matcher semantics preserved in the DSL migration

I verified the rewritten chain is semantically equivalent to the old one: antMatchers(HttpMethod.OPTIONS) (null pattern = any path) maps to requestMatchers(HttpMethod.OPTIONS, "/**"), and the ordering /articles/feed (authenticated) before /articles/** (permitAll) is preserved, so the feed endpoint still requires auth. Note the newly added /graphiql/** permit rule widens the public surface to every GraphiQL sub-path, which is intended per the description but does expose the whole prefix.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);

http.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import io.spring.core.article.Article;
import io.spring.core.article.ArticleRepository;
import io.spring.core.user.User;
import javax.validation.Valid;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package io.spring.application.article;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;

@Documented
@Constraint(validatedBy = DuplicatedArticleValidator.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import io.spring.application.ArticleQueryService;
import io.spring.core.article.Article;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.beans.factory.annotation.Autowired;

class DuplicatedArticleValidator
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.spring.application.article;

import com.fasterxml.jackson.annotation.JsonRootName;
import jakarta.validation.constraints.NotBlank;
import java.util.List;
import javax.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package io.spring.application.user;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.validation.Constraint;
import javax.validation.Payload;

@Constraint(validatedBy = DuplicatedEmailValidator.class)
@Retention(RetentionPolicy.RUNTIME)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.spring.application.user;

import io.spring.core.user.UserRepository;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.beans.factory.annotation.Autowired;

public class DuplicatedEmailValidator
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package io.spring.application.user;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.validation.Constraint;
import javax.validation.Payload;

@Constraint(validatedBy = DuplicatedUsernameValidator.class)
@Retention(RetentionPolicy.RUNTIME)
Expand Down
Loading
Loading