From 5f7c911fa9c576d50d2f9f93f205d6b1adc10d03 Mon Sep 17 00:00:00 2001 From: efpcode Date: Wed, 9 Apr 2025 18:43:20 +0200 Subject: [PATCH 01/51] (feature) Adds new dependency and initial SecurityConfig file Co-authored-by: Sedya seyda.kinaci@iths.se> --- pom.xml | 17 ++++++++++ .../barkr/config/SecurityConfig.java | 32 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 src/main/java/org/storkforge/barkr/config/SecurityConfig.java diff --git a/pom.xml b/pom.xml index 1c9891c..eb1dcfd 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,23 @@ org.flywaydb flyway-database-postgresql + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security + spring-security-test + test + + + org.thymeleaf.extras + thymeleaf-extras-springsecurity6 + + + org.springframework.boot + spring-boot-starter-oauth2-client + org.springframework.boot spring-boot-devtools diff --git a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java new file mode 100644 index 0000000..4ac38e9 --- /dev/null +++ b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java @@ -0,0 +1,32 @@ +package org.storkforge.barkr.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableMethodSecurity +public class SecurityConfig { + + @Bean + @Order(1) + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{ + http + .formLogin(Customizer.withDefaults()) + //.oauth2Login(Customizer.withDefaults()) + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/","/login", "/error").permitAll() + .requestMatchers("/profile/**").authenticated() + .requestMatchers("/post/add").authenticated() + .requestMatchers("/account/**/*").authenticated() + .requestMatchers("/**/{username}").authenticated() + .anyRequest().denyAll() + + ); + return http.build(); + } + } From 9a803fb1906c0beb803c7b8b148efeab05346e92 Mon Sep 17 00:00:00 2001 From: efpcode Date: Mon, 14 Apr 2025 15:27:16 +0200 Subject: [PATCH 02/51] removes env + fixes things --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 526255e..07d4390 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,4 @@ build/ ### VS Code ### .vscode/ -.env \ No newline at end of file +.env/ From ea3d752516e03fbb0450d7fed72eacc19a5ebb33 Mon Sep 17 00:00:00 2001 From: efpcode Date: Mon, 14 Apr 2025 15:53:18 +0200 Subject: [PATCH 03/51] Adds import env + debug Co-authored-by: seyda.kinaci@iths.se --- src/main/resources/application-dev.properties | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index d66ef93..c0a13d3 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -2,6 +2,8 @@ spring.docker.compose.lifecycle-management=start_only spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true spring.graphql.graphiql.enabled=true + +logging.level.org.springframework.security=DEBUG spring.config.import=file:.env[.properties] spring.ai.mistralai.base-url=https://api.mistral.ai/v1 -spring.ai.mistralai.api-key=${MISTRAL_API_KEY} \ No newline at end of file +spring.ai.mistralai.api-key=${MISTRAL_API_KEY} From d94aa7888110831bd7c9132a4c04c0d1440b69a5 Mon Sep 17 00:00:00 2001 From: efpcode Date: Mon, 14 Apr 2025 15:59:00 +0200 Subject: [PATCH 04/51] Adds removes env again --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 07d4390..221c5f3 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,4 @@ build/ ### VS Code ### .vscode/ -.env/ +.env From 51b5a8483326ed8da0325b8a45e1eae2c83aa692 Mon Sep 17 00:00:00 2001 From: efpcode Date: Mon, 14 Apr 2025 16:04:00 +0200 Subject: [PATCH 05/51] (feature) Adds dev-properties for google oauth2 Co-authored-by: seyda.kinaci@iths.se --- src/main/resources/application-dev.properties | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index c0a13d3..9012aa7 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -7,3 +7,9 @@ logging.level.org.springframework.security=DEBUG spring.config.import=file:.env[.properties] spring.ai.mistralai.base-url=https://api.mistral.ai/v1 spring.ai.mistralai.api-key=${MISTRAL_API_KEY} + + +spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID} +spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET} +spring.security.oauth2.client.registration.google.scope=openid, profile, email +spring.security.oauth2.client.registration.google.redirect-uri={baseUrl}/login/oauth2/code/google From 35ec3ee8849370c36731300fe2562506b53b4463 Mon Sep 17 00:00:00 2001 From: efpcode Date: Mon, 14 Apr 2025 16:05:52 +0200 Subject: [PATCH 06/51] (fix) removes formlogin from SecurityConfigFile Co-authored-by: seyda.kinaci@iths.se --- src/main/java/org/storkforge/barkr/config/SecurityConfig.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java index 4ac38e9..f076c11 100644 --- a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java +++ b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java @@ -16,8 +16,7 @@ public class SecurityConfig { @Order(1) public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{ http - .formLogin(Customizer.withDefaults()) - //.oauth2Login(Customizer.withDefaults()) + .oauth2Login(Customizer.withDefaults()) .authorizeHttpRequests(authorize -> authorize .requestMatchers("/","/login", "/error").permitAll() .requestMatchers("/profile/**").authenticated() From 35c4ca2c4678897c917dac72b6013108d6d71ca5 Mon Sep 17 00:00:00 2001 From: efpcode Date: Mon, 14 Apr 2025 21:46:26 +0200 Subject: [PATCH 07/51] (feature) fixes endpoints authfiltering --- .../org/storkforge/barkr/config/SecurityConfig.java | 10 ++++++---- src/main/resources/application-dev.properties | 2 ++ src/main/resources/templates/fragments/header.html | 2 +- src/main/resources/templates/index.html | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java index f076c11..e012e78 100644 --- a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java +++ b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java @@ -3,6 +3,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; +import org.springframework.http.HttpMethod; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -18,11 +19,12 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti http .oauth2Login(Customizer.withDefaults()) .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/","/login", "/error").permitAll() - .requestMatchers("/profile/**").authenticated() + .requestMatchers("/","/login", "/error", "/css/**", "/js/**", "/images/**").permitAll() + .requestMatchers( "/account/{id}/image").permitAll() + .requestMatchers("/ai/generate").authenticated() .requestMatchers("/post/add").authenticated() - .requestMatchers("/account/**/*").authenticated() - .requestMatchers("/**/{username}").authenticated() + .requestMatchers("/account/{id}/upload").authenticated() + .requestMatchers("/{username}").authenticated() .anyRequest().denyAll() ); diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 9012aa7..79e835f 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -4,6 +4,8 @@ spring.jpa.properties.hibernate.format_sql=true spring.graphql.graphiql.enabled=true logging.level.org.springframework.security=DEBUG +logging.level.org.springframework.security.oauth2=DEBUG + spring.config.import=file:.env[.properties] spring.ai.mistralai.base-url=https://api.mistral.ai/v1 spring.ai.mistralai.api-key=${MISTRAL_API_KEY} diff --git a/src/main/resources/templates/fragments/header.html b/src/main/resources/templates/fragments/header.html index 292fdc2..6566788 100644 --- a/src/main/resources/templates/fragments/header.html +++ b/src/main/resources/templates/fragments/header.html @@ -22,4 +22,4 @@

Barkr

- \ No newline at end of file + diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 4dc49eb..b41f269 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -52,4 +52,4 @@

Create a New Bark

}); - \ No newline at end of file + From 036eb0c077ebfd482541e22a523e53067937b800 Mon Sep 17 00:00:00 2001 From: efpcode Date: Tue, 15 Apr 2025 12:39:00 +0200 Subject: [PATCH 08/51] (feature) adds new OidcClass for mapping user-metadata --- .../barkr/config/SecurityConfig.java | 1 - .../barkr/domain/CustomOidc2UserService.java | 31 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java diff --git a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java index e012e78..c44a18a 100644 --- a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java +++ b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java @@ -3,7 +3,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; -import org.springframework.http.HttpMethod; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; diff --git a/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java b/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java new file mode 100644 index 0000000..dca5706 --- /dev/null +++ b/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java @@ -0,0 +1,31 @@ +package org.storkforge.barkr.domain; + +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.stereotype.Service; + +@Service +public class CustomOidc2UserService extends OidcUserService { + + @Override + public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException{ + OidcUser user = super.loadUser(userRequest); + System.out.println("###########################################################################################################"); + System.out.println("The OIDC user found:"); + System.out.println(user); + System.out.println("###########################################################################################################"); + System.out.println(user.getName()); + System.out.println(user.getEmail()); + System.out.println("###########################################################################################################"); + + //TODO For Servic + // - 1. Check that user exists in db with lookup on user.getName() which is a unique value. + // - 2. If user exists retrieve user object and if not create new user entity. + + return user; + + } + +} From 5e12d764e663bedfc63997a3152214fd23292471 Mon Sep 17 00:00:00 2001 From: efpcode Date: Tue, 15 Apr 2025 13:58:56 +0200 Subject: [PATCH 09/51] (todo) Adds placeholder code for apikey filtering and validation --- pom.xml | 5 ++ .../barkr/config/SecurityConfig.java | 29 +++++++++++- .../filters/ApiKeyAuthenticationFilter.java | 31 ++++++++++++ .../filters/ApiKeyAuthenticationToken.java | 47 +++++++++++++++++++ 4 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationFilter.java create mode 100644 src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationToken.java diff --git a/pom.xml b/pom.xml index eb1dcfd..153919c 100644 --- a/pom.xml +++ b/pom.xml @@ -137,6 +137,11 @@ org.apache.maven.plugins maven-compiler-plugin + + 24 + 24 + --enable-preview + org.hibernate.orm.tooling diff --git a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java index c44a18a..330223b 100644 --- a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java +++ b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java @@ -3,17 +3,21 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; +import org.springframework.http.HttpMethod; import org.springframework.security.config.Customizer; 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.configurers.AbstractHttpConfigurer; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.storkforge.barkr.filters.ApiKeyAuthenticationFilter; @Configuration @EnableMethodSecurity public class SecurityConfig { @Bean - @Order(1) + @Order(2) public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{ http .oauth2Login(Customizer.withDefaults()) @@ -29,4 +33,27 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti ); return http.build(); } + + + @Bean + @Order(1) + public SecurityFilterChain restAPIAndGraphQLFilterChain(HttpSecurity http) throws Exception{ + http.securityMatcher("/api/**", "/graphql") + .csrf(AbstractHttpConfigurer::disable) + .addFilterAfter(new ApiKeyAuthenticationFilter(), LogoutFilter.class) + .authorizeHttpRequests( + authorize -> authorize + .requestMatchers("/api/accounts").authenticated() + .requestMatchers("/api/posts").authenticated() + .requestMatchers("/accounts/{id}").authenticated() + .requestMatchers("/api/posts/{id}").authenticated() + .requestMatchers(HttpMethod.POST, "/graphql").authenticated() + .anyRequest().denyAll() + + ); + return http.build(); + } + + + } diff --git a/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationFilter.java b/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationFilter.java new file mode 100644 index 0000000..4b6e3bb --- /dev/null +++ b/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationFilter.java @@ -0,0 +1,31 @@ +package org.storkforge.barkr.filters; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class ApiKeyAuthenticationFilter extends OncePerRequestFilter { + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + + String apiKey = request.getHeader("API-KEY"); + + if (isValidApiKey(apiKey)) { + Authentication auth = new ApiKeyAuthenticationToken(apiKey); + SecurityContextHolder.getContext().setAuthentication(auth); + } + filterChain.doFilter(request, response); + + + } + + private boolean isValidApiKey(String apiKey){ + return "mykey".equals(apiKey); + } +} diff --git a/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationToken.java b/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationToken.java new file mode 100644 index 0000000..73d11b7 --- /dev/null +++ b/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationToken.java @@ -0,0 +1,47 @@ +package org.storkforge.barkr.filters; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { + private final String apiKey; + + public ApiKeyAuthenticationToken(String apiKey) { + List authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_APIKEY")); + super(authorities); + this.apiKey = apiKey; + super.setAuthenticated(true); + } + + public String getApiKey() { + return apiKey; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return apiKey; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + ApiKeyAuthenticationToken that = (ApiKeyAuthenticationToken) o; + return Objects.equals(apiKey, that.apiKey); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), apiKey); + } +} From 2068722ce2a966f995c0bc5ef1ec37dd64bb7f1f Mon Sep 17 00:00:00 2001 From: efpcode Date: Tue, 15 Apr 2025 14:05:28 +0200 Subject: [PATCH 10/51] (fixes) .env imports with optional now --- src/main/resources/application-dev.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 8da70ea..74f5357 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -7,6 +7,7 @@ logging.level.org.springframework.security=DEBUG logging.level.org.springframework.security.oauth2=DEBUG spring.config.import=optional:file:.env[.properties] + spring.ai.mistralai.base-url=https://api.mistral.ai/v1 spring.ai.mistralai.api-key=${MISTRAL_API_KEY} From f559892b057eb15f0730fb380d7c5f0008261652 Mon Sep 17 00:00:00 2001 From: efpcode Date: Tue, 15 Apr 2025 14:31:44 +0200 Subject: [PATCH 11/51] (fixes) faulty endpoint --- src/main/java/org/storkforge/barkr/config/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java index 330223b..d0479e0 100644 --- a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java +++ b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java @@ -45,7 +45,7 @@ public SecurityFilterChain restAPIAndGraphQLFilterChain(HttpSecurity http) throw authorize -> authorize .requestMatchers("/api/accounts").authenticated() .requestMatchers("/api/posts").authenticated() - .requestMatchers("/accounts/{id}").authenticated() + .requestMatchers("/api/accounts/{id}").authenticated() .requestMatchers("/api/posts/{id}").authenticated() .requestMatchers(HttpMethod.POST, "/graphql").authenticated() .anyRequest().denyAll() From 00ad31d32af12c56c9bda26bb6299eef565cf878 Mon Sep 17 00:00:00 2001 From: efpcode Date: Tue, 15 Apr 2025 16:49:24 +0200 Subject: [PATCH 12/51] !(fix) test alterations to pass Securityfilteration and other minor fixes --- pom.xml | 5 ----- .../barkr/filters/ApiKeyAuthenticationToken.java | 6 ++---- .../api/controller/ApiAccountControllerTest.java | 6 +++++- .../api/controller/ApiPostControllerTest.java | 5 ++++- .../barkr/graphql/AccountResolverTest.java | 4 ++++ .../barkr/graphql/PostResolverTest.java | 4 ++++ .../controller/WebControllerImageRouteTest.java | 16 ++++++++++++---- .../barkr/web/controller/WebControllerTest.java | 6 ++++++ 8 files changed, 37 insertions(+), 15 deletions(-) diff --git a/pom.xml b/pom.xml index 86b6e48..56c8da4 100644 --- a/pom.xml +++ b/pom.xml @@ -157,11 +157,6 @@ org.apache.maven.plugins maven-compiler-plugin - - 24 - 24 - --enable-preview - org.hibernate.orm.tooling diff --git a/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationToken.java b/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationToken.java index 73d11b7..fd131d4 100644 --- a/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationToken.java +++ b/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationToken.java @@ -1,19 +1,17 @@ package org.storkforge.barkr.filters; import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import java.util.Collections; -import java.util.List; import java.util.Objects; public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { private final String apiKey; public ApiKeyAuthenticationToken(String apiKey) { - List authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_APIKEY")); - super(authorities); + // Compatibility issues flexiable constructor not working + super(Collections.singletonList(new SimpleGrantedAuthority("ROLE_APIKEY"))); this.apiKey = apiKey; super.setAuthenticated(true); } diff --git a/src/test/java/org/storkforge/barkr/api/controller/ApiAccountControllerTest.java b/src/test/java/org/storkforge/barkr/api/controller/ApiAccountControllerTest.java index 1093ee7..04d7f49 100644 --- a/src/test/java/org/storkforge/barkr/api/controller/ApiAccountControllerTest.java +++ b/src/test/java/org/storkforge/barkr/api/controller/ApiAccountControllerTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.storkforge.barkr.domain.AccountService; @@ -31,6 +32,7 @@ class ApiAccountControllerTest { @Test @DisplayName("Returns all accounts from service") + @WithMockUser void accountsReturnsAllAccounts() throws Exception { List mockAccounts = List.of( new ResponseAccount(1L, "testaccount", LocalDateTime.now(), "beagle", new byte[0]), @@ -47,6 +49,7 @@ void accountsReturnsAllAccounts() throws Exception { } @Test + @WithMockUser void findAccount() throws Exception { ResponseAccount mockAccount = new ResponseAccount(1L, "testAccount", LocalDateTime.now(), "beagle", new byte[0]); when(service.findById(1L)).thenReturn(mockAccount); @@ -60,6 +63,7 @@ void findAccount() throws Exception { } @Test + @WithMockUser @DisplayName("Handles error for nonexistent id") void handlesErrorForNonexistentId() throws Exception { when(service.findById(1L)).thenThrow(new AccountNotFound("Account with id: 1 was not found")); @@ -68,4 +72,4 @@ void handlesErrorForNonexistentId() throws Exception { .andDo(print()) .andExpect((status().isNotFound())); } -} \ No newline at end of file +} diff --git a/src/test/java/org/storkforge/barkr/api/controller/ApiPostControllerTest.java b/src/test/java/org/storkforge/barkr/api/controller/ApiPostControllerTest.java index 57267e3..8083fb5 100644 --- a/src/test/java/org/storkforge/barkr/api/controller/ApiPostControllerTest.java +++ b/src/test/java/org/storkforge/barkr/api/controller/ApiPostControllerTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.storkforge.barkr.domain.PostService; @@ -29,6 +30,7 @@ class ApiPostControllerTest { private PostService service; @Test + @WithMockUser @DisplayName("Finds all posts from service") void postsReturnsAllPosts() throws Exception { ResponseAccount mockAccount = new ResponseAccount(1L, "testAccount", LocalDateTime.now(), "beagle", new byte[0]); @@ -48,6 +50,7 @@ void postsReturnsAllPosts() throws Exception { } @Test + @WithMockUser @DisplayName("Finds a post from service") void findPost() throws Exception { ResponseAccount mockAccount = new ResponseAccount(1L, "testAccount", LocalDateTime.now(), "beagle", new byte[0]); @@ -65,6 +68,7 @@ void findPost() throws Exception { @Test + @WithMockUser @DisplayName("Handles error for nonexistent id") void handlesErrorForNonexistentId() throws Exception { when(service.findById(1L)).thenThrow(new PostNotFound("Post with id: 1 was not found")); @@ -74,4 +78,3 @@ void handlesErrorForNonexistentId() throws Exception { .andExpect(status().isNotFound()); } } - diff --git a/src/test/java/org/storkforge/barkr/graphql/AccountResolverTest.java b/src/test/java/org/storkforge/barkr/graphql/AccountResolverTest.java index 36ed94c..7073fd8 100644 --- a/src/test/java/org/storkforge/barkr/graphql/AccountResolverTest.java +++ b/src/test/java/org/storkforge/barkr/graphql/AccountResolverTest.java @@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.graphql.GraphQlTest; import org.springframework.graphql.test.tester.GraphQlTester; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.storkforge.barkr.domain.AccountService; import org.storkforge.barkr.dto.accountDto.ResponseAccount; @@ -30,6 +31,7 @@ class AccountResolverTest { private AccountService accountService; @Test + @WithMockUser void testGetAccountById() { ResponseAccount account = new ResponseAccount(10L, "testAccount", LocalDateTime.now(), "beagle", new byte[0]); @@ -49,6 +51,7 @@ void testGetAccountById() { } @Test + @WithMockUser void testGetAllAccounts() { ResponseAccount account1 = new ResponseAccount(1L, "accountOne", LocalDateTime.now(),"beagle", new byte[0]); ResponseAccount account2 = new ResponseAccount(2L, "accountTwo", LocalDateTime.now(), "beagle", new byte[0]); @@ -71,6 +74,7 @@ void testGetAllAccounts() { @Test @DisplayName("Handles error for nonexistent id") + @WithMockUser void handlesErrorForNonexistentId() { when(accountService.findById(1L)).thenThrow(new AccountNotFound("Account with id: 1 was not found")); diff --git a/src/test/java/org/storkforge/barkr/graphql/PostResolverTest.java b/src/test/java/org/storkforge/barkr/graphql/PostResolverTest.java index 592f607..f8a4538 100644 --- a/src/test/java/org/storkforge/barkr/graphql/PostResolverTest.java +++ b/src/test/java/org/storkforge/barkr/graphql/PostResolverTest.java @@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.graphql.GraphQlTest; import org.springframework.graphql.test.tester.GraphQlTester; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.storkforge.barkr.domain.AccountService; import org.storkforge.barkr.domain.PostService; @@ -32,6 +33,7 @@ class PostResolverTest { private AccountService accountService; @Test + @WithMockUser void testGetPostById() { ResponseAccount account = new ResponseAccount(10L, "testAccount", LocalDateTime.now(), "beagle", new byte[0]); ResponsePost post = new ResponsePost(1L, "Test content", account, LocalDateTime.now()); @@ -58,6 +60,7 @@ void testGetPostById() { } @Test + @WithMockUser void testGetAllPosts() { ResponseAccount account = new ResponseAccount(10L, "testAccount", LocalDateTime.now(), "beagle", new byte[0]); ResponsePost post1 = new ResponsePost(1L, "First post", account, LocalDateTime.now()); @@ -80,6 +83,7 @@ void testGetAllPosts() { } @Test + @WithMockUser @DisplayName("Handles error for nonexistent id") void handlesErrorForNonexistentId() { when(postService.findById(1L)).thenThrow(new PostNotFound("Post with id: 1 was not found")); diff --git a/src/test/java/org/storkforge/barkr/web/controller/WebControllerImageRouteTest.java b/src/test/java/org/storkforge/barkr/web/controller/WebControllerImageRouteTest.java index 8d0fb19..c25bb99 100644 --- a/src/test/java/org/storkforge/barkr/web/controller/WebControllerImageRouteTest.java +++ b/src/test/java/org/storkforge/barkr/web/controller/WebControllerImageRouteTest.java @@ -6,6 +6,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.storkforge.barkr.domain.AccountService; @@ -17,6 +18,7 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -37,6 +39,7 @@ class WebControllerImageRouteTest { @Test @DisplayName("Returns account image if present") + @WithMockUser void returnsAccountImageIfPresent() throws Exception { byte[] image = "test-image".getBytes(); when(accountService.getAccountImage(1L)).thenReturn(image); @@ -48,6 +51,7 @@ void returnsAccountImageIfPresent() throws Exception { } @Test + @WithMockUser @DisplayName("Returns fallback image if account image is null") void returnsFallbackImageIfNull() throws Exception { byte[] fallback; @@ -65,39 +69,43 @@ void returnsFallbackImageIfNull() throws Exception { @Test @DisplayName("Redirects with error if file is empty") + @WithMockUser void fileIsEmpty() throws Exception { MockMultipartFile emptyFile = new MockMultipartFile("file", "image.png", "image/png", new byte[0]); - mockMvc.perform(multipart("/account/1/upload").file(emptyFile)) + mockMvc.perform(multipart("/account/1/upload").file(emptyFile).with(csrf())) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl("/")) .andExpect(flash().attribute("error", "File is empty")); } @Test + @WithMockUser @DisplayName("Redirects with error if file is not an image") void fileIsNotImage() throws Exception { MockMultipartFile file = new MockMultipartFile("file", "file.txt", "text/plain", "some-text".getBytes()); - mockMvc.perform(multipart("/account/1/upload").file(file)) + mockMvc.perform(multipart("/account/1/upload").file(file).with(csrf())) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl("/")) .andExpect(flash().attribute("error", "File is not an image")); } @Test + @WithMockUser @DisplayName("Redirects with error if file is too large") void fileIsTooLarge() throws Exception { byte[] large = new byte[6 * 1024 * 1024]; // 6MB MockMultipartFile file = new MockMultipartFile("file", "big.png", "image/png", large); - mockMvc.perform(multipart("/account/1/upload").file(file)) + mockMvc.perform(multipart("/account/1/upload").file(file).with(csrf())) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl("/")) .andExpect(flash().attribute("error", "File is too large")); } @Test + @WithMockUser @DisplayName("Redirects to root after successful image upload") void uploadSuccess() throws Exception { byte[] img = "image-content".getBytes(); @@ -105,7 +113,7 @@ void uploadSuccess() throws Exception { doNothing().when(accountService).updateImage(1L, img); - mockMvc.perform(multipart("/account/1/upload").file(file)) + mockMvc.perform(multipart("/account/1/upload").file(file).with(csrf())) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl("/")); } diff --git a/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java b/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java index 387cdb4..a2bb58c 100644 --- a/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java +++ b/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java @@ -9,6 +9,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import org.storkforge.barkr.domain.entity.Account; @@ -97,6 +98,7 @@ void verifyAddPostFormExist() throws IOException { } @Test + @WithMockUser @DisplayName("Can submit the add post form") void verifyAddPostFormSubmitted() throws IOException { HtmlPage page = htmlClient.getPage("/"); @@ -117,6 +119,7 @@ void verifyAddPostFormSubmitted() throws IOException { } @Test + @WithMockUser @DisplayName("Redirects the user on empty form value") void redirectOnEmptyForm() throws IOException { HtmlPage page = htmlClient.getPage("/"); @@ -130,6 +133,7 @@ void redirectOnEmptyForm() throws IOException { } @Test + @WithMockUser @DisplayName("Redirects the user if value is greater than 255 characters") void redirectOnTooManyCharactersInput() throws IOException { HtmlPage page = htmlClient.getPage("/"); @@ -150,6 +154,7 @@ void redirectOnTooManyCharactersInput() throws IOException { @Nested class ProfileRouteTest { @Test + @WithMockUser @DisplayName("Can view profile page") void viewProfilePage() throws IOException { Account mockAccount = new Account(); @@ -182,6 +187,7 @@ void viewProfilePage() throws IOException { } @Test + @WithMockUser @DisplayName("Redirects the user on nonexistent account") void redirectsOnNonexistentAccount() throws IOException { HtmlPage page = htmlClient.getPage("/nonExistent"); From 83b6c9c65e102d084d0ab3fcafe4f996d616a519 Mon Sep 17 00:00:00 2001 From: efpcode Date: Thu, 17 Apr 2025 09:54:43 +0200 Subject: [PATCH 13/51] !(feature) new entites for handling Apikeys --- .../api/controller/ApiAccountController.java | 1 + .../barkr/config/SecurityConfig.java | 1 + .../barkr/domain/entity/Account.java | 40 ++++- .../entity/GoogleAccountApiKeyLink.java | 75 +++++++++ .../barkr/domain/entity/IssuedApiKey.java | 154 ++++++++++++++++++ 5 files changed, 266 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/storkforge/barkr/domain/entity/GoogleAccountApiKeyLink.java create mode 100644 src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java diff --git a/src/main/java/org/storkforge/barkr/api/controller/ApiAccountController.java b/src/main/java/org/storkforge/barkr/api/controller/ApiAccountController.java index d7fc91e..8a9395a 100644 --- a/src/main/java/org/storkforge/barkr/api/controller/ApiAccountController.java +++ b/src/main/java/org/storkforge/barkr/api/controller/ApiAccountController.java @@ -3,6 +3,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; +import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java index d0479e0..ed02be6 100644 --- a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java +++ b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java @@ -23,6 +23,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .oauth2Login(Customizer.withDefaults()) .authorizeHttpRequests(authorize -> authorize .requestMatchers("/","/login", "/error", "/css/**", "/js/**", "/images/**").permitAll() + .requestMatchers("/post/load").permitAll() .requestMatchers( "/account/{id}/image").permitAll() .requestMatchers("/ai/generate").authenticated() .requestMatchers("/post/add").authenticated() diff --git a/src/main/java/org/storkforge/barkr/domain/entity/Account.java b/src/main/java/org/storkforge/barkr/domain/entity/Account.java index bf0f5f7..c53fa9e 100644 --- a/src/main/java/org/storkforge/barkr/domain/entity/Account.java +++ b/src/main/java/org/storkforge/barkr/domain/entity/Account.java @@ -34,9 +34,34 @@ public class Account implements Serializable { @Size(min = 2, max = 100, message = "Breed name must be between 2 and 100 characters") private String breed; + @NotBlank + @Column(unique = true, nullable = false, updatable = false) + private String googleOidc2Id; + + @OneToMany(mappedBy = "account", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private List posts = new ArrayList<>(); + @OneToOne(fetch = FetchType.LAZY, mappedBy = "account", cascade = CascadeType.ALL) + private GoogleAccountApiKeyLink googleAccountApiKeyLink; + + + public GoogleAccountApiKeyLink getGoogleAccountApiKeyLink() { + return googleAccountApiKeyLink; + } + + public void setGoogleAccountApiKeyLink(GoogleAccountApiKeyLink googleAccountApiKeyLink) { + this.googleAccountApiKeyLink = googleAccountApiKeyLink; + } + + public String getGoogleOidc2Id() { + return googleOidc2Id; + } + + public void setGoogleOidc2Id(String googleOidc2Id) { + this.googleOidc2Id = googleOidc2Id; + } + @PrePersist protected void onCreate() { if (createdAt == null) createdAt = LocalDateTime.now(); @@ -107,12 +132,17 @@ public final int hashCode() { return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); } + + + @Override public String toString() { - return getClass().getSimpleName() + "(" + - "id = " + id + ", " + - "username = " + username + ", " + - "createdAt = " + createdAt + ", " + - "breed = " + breed + ")"; + return getClass().getSimpleName() + "(" + + "id = " + id + ", " + + "username = " + username + ", " + + "createdAt = " + createdAt + ", " + + "image = " + image + ", " + + "breed = " + breed + ", " + + "googleOidc2Id = " + googleOidc2Id + ")"; } } diff --git a/src/main/java/org/storkforge/barkr/domain/entity/GoogleAccountApiKeyLink.java b/src/main/java/org/storkforge/barkr/domain/entity/GoogleAccountApiKeyLink.java new file mode 100644 index 0000000..7190217 --- /dev/null +++ b/src/main/java/org/storkforge/barkr/domain/entity/GoogleAccountApiKeyLink.java @@ -0,0 +1,75 @@ +package org.storkforge.barkr.domain.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import org.hibernate.proxy.HibernateProxy; + +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; + +@Entity +@Table(name = "google_account_api_key_link") +public class GoogleAccountApiKeyLink { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "google_oidc2_id", referencedColumnName = "googleOidc2Id", nullable = false, unique = true, updatable = false) + private Account account; + + @OneToMany(mappedBy = "googleAccountApiKeyLink", cascade = CascadeType.ALL, orphanRemoval = true) + private Set issuedApiKeys = new LinkedHashSet<>(); + + public Set getIssuedApiKeys() { + return issuedApiKeys; + } + + public void setIssuedApiKeys(Set issuedApiKeys) { + this.issuedApiKeys = issuedApiKeys; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); + Class thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + GoogleAccountApiKeyLink that = (GoogleAccountApiKeyLink) o; + return getId() != null && Objects.equals(getId(), that.getId()); + } + + @Override + public final int hashCode() { + return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + + "id = " + id + ")"; + } + + +} diff --git a/src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java b/src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java new file mode 100644 index 0000000..0d12686 --- /dev/null +++ b/src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java @@ -0,0 +1,154 @@ +package org.storkforge.barkr.domain.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PastOrPresent; +import org.hibernate.proxy.HibernateProxy; + +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity +@Table(name = "issued_api_key") +public class IssuedApiKey { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false, updatable = false) + private Long id; + + @NotBlank + @Column(nullable = false, updatable = false, unique = true) + private String hashedApiKey; + + @Column(nullable = false, updatable = false) + @NotNull + @PastOrPresent + private LocalDateTime issuedAt; + + @Column(nullable = false, updatable = false) + @NotNull + @FutureOrPresent + private LocalDateTime expiresAt; + + @Column(nullable = false) + private boolean revoked = false; + + @Column(nullable = false) + @NotNull + @FutureOrPresent + private LocalDateTime lastUsedAt; + + @Column(nullable = false) + @NotBlank + private String apiKeyName; + + @NotNull + @ManyToOne + @JoinColumn(name = "google_account_api_key_link_id") + private GoogleAccountApiKeyLink googleAccountApiKeyLink; + + + public GoogleAccountApiKeyLink getGoogleAccountApiKeyLink() { + return googleAccountApiKeyLink; + } + + public void setGoogleAccountApiKeyLink(GoogleAccountApiKeyLink googleAccountApiKeyLink) { + this.googleAccountApiKeyLink = googleAccountApiKeyLink; + } + + @PrePersist + protected void onCreate() { + if(issuedAt == null) issuedAt = LocalDateTime.now(); + if(expiresAt == null) expiresAt = issuedAt.plusMinutes(5); + if(lastUsedAt == null) lastUsedAt = issuedAt; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getHashedApiKey() { + return hashedApiKey; + } + + public void setHashedApiKey(String hashedApiKey) { + this.hashedApiKey = hashedApiKey; + } + + public LocalDateTime getIssuedAt() { + return issuedAt; + } + + public void setIssuedAt(LocalDateTime issuedAt) { + this.issuedAt = issuedAt; + } + + public LocalDateTime getExpiresAt() { + return expiresAt; + } + + public void setExpiresAt(LocalDateTime expiresAt) { + this.expiresAt = expiresAt; + } + + public boolean isRevoked() { + return revoked; + } + + public void setRevoked(boolean revoked) { + this.revoked = revoked; + } + + public LocalDateTime getLastUsedAt() { + return lastUsedAt; + } + + public void setLastUsedAt(LocalDateTime lastUsedAt) { + this.lastUsedAt = lastUsedAt; + } + + public String getApiKeyName() { + return apiKeyName; + } + + public void setApiKeyName(String apiKeyName) { + this.apiKeyName = apiKeyName; + } + + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); + Class thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + IssuedApiKey that = (IssuedApiKey) o; + return getId() != null && Objects.equals(getId(), that.getId()); + } + + @Override + public final int hashCode() { + return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); + } + + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + + "id = " + id + ", " + + "hashedApiKey = " + hashedApiKey + ", " + + "issuedAt = " + issuedAt + ", " + + "expiresAt = " + expiresAt + ", " + + "revoked = " + revoked + ", " + + "lastUsedAt = " + lastUsedAt + ", " + + "apiKeyName = " + apiKeyName + ", " + + "googleAccountApiKeyLink = " + googleAccountApiKeyLink + ")"; + } +} From 73c243d0ba056cce4e886064ac95ade5ba86de7c Mon Sep 17 00:00:00 2001 From: efpcode Date: Thu, 17 Apr 2025 10:01:40 +0200 Subject: [PATCH 14/51] (fix) adds serializable to new entites --- .../barkr/domain/entity/GoogleAccountApiKeyLink.java | 3 ++- .../java/org/storkforge/barkr/domain/entity/IssuedApiKey.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/storkforge/barkr/domain/entity/GoogleAccountApiKeyLink.java b/src/main/java/org/storkforge/barkr/domain/entity/GoogleAccountApiKeyLink.java index 7190217..34c093f 100644 --- a/src/main/java/org/storkforge/barkr/domain/entity/GoogleAccountApiKeyLink.java +++ b/src/main/java/org/storkforge/barkr/domain/entity/GoogleAccountApiKeyLink.java @@ -4,13 +4,14 @@ import jakarta.validation.constraints.NotNull; import org.hibernate.proxy.HibernateProxy; +import java.io.Serializable; import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; @Entity @Table(name = "google_account_api_key_link") -public class GoogleAccountApiKeyLink { +public class GoogleAccountApiKeyLink implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java b/src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java index 0d12686..3dda712 100644 --- a/src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java +++ b/src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java @@ -7,12 +7,13 @@ import jakarta.validation.constraints.PastOrPresent; import org.hibernate.proxy.HibernateProxy; +import java.io.Serializable; import java.time.LocalDateTime; import java.util.Objects; @Entity @Table(name = "issued_api_key") -public class IssuedApiKey { +public class IssuedApiKey implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(nullable = false, updatable = false) From 22a64cbe9d813322b3850432cde5fee5efe87d07 Mon Sep 17 00:00:00 2001 From: efpcode Date: Thu, 17 Apr 2025 10:48:39 +0200 Subject: [PATCH 15/51] !(feature) Adds migration for new db entites --- ...ts_on_username_and_google_oidc2id.sql .sql | 19 +++++++++++++++++++ ...eate_google_account_api_key_link_table.sql | 10 ++++++++++ .../V5__Create_issued_api_key_table.sql .sql | 18 ++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 src/main/resources/db/migration/V3__Add_unique_constraints_on_username_and_google_oidc2id.sql .sql create mode 100644 src/main/resources/db/migration/V4__Create_google_account_api_key_link_table.sql create mode 100644 src/main/resources/db/migration/V5__Create_issued_api_key_table.sql .sql diff --git a/src/main/resources/db/migration/V3__Add_unique_constraints_on_username_and_google_oidc2id.sql .sql b/src/main/resources/db/migration/V3__Add_unique_constraints_on_username_and_google_oidc2id.sql .sql new file mode 100644 index 0000000..0097887 --- /dev/null +++ b/src/main/resources/db/migration/V3__Add_unique_constraints_on_username_and_google_oidc2id.sql .sql @@ -0,0 +1,19 @@ +CREATE TABLE account +( + account_id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + username VARCHAR(255) NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + image BYTEA, + breed VARCHAR(100), + google_oidc2id VARCHAR(255) NOT NULL, + CONSTRAINT pk_account PRIMARY KEY (account_id) +); + +ALTER TABLE account + ADD CONSTRAINT uc_account_googleoidc2id UNIQUE (google_oidc2id); + +ALTER TABLE account + ADD CONSTRAINT uc_account_username UNIQUE (username); + +-- Add unique index for performance optimization +CREATE UNIQUE INDEX idx_account_google_oidc2_id ON account (google_oidc2id); diff --git a/src/main/resources/db/migration/V4__Create_google_account_api_key_link_table.sql b/src/main/resources/db/migration/V4__Create_google_account_api_key_link_table.sql new file mode 100644 index 0000000..1f81404 --- /dev/null +++ b/src/main/resources/db/migration/V4__Create_google_account_api_key_link_table.sql @@ -0,0 +1,10 @@ +CREATE TABLE google_account_api_key_link +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + google_oidc2_id VARCHAR(255) NOT NULL, + CONSTRAINT pk_google_account_api_key_link PRIMARY KEY (id), + CONSTRAINT fk_gaakl_account_google_oidc2_id FOREIGN KEY (google_oidc2_id) REFERENCES account (google_oidc2id) +); + +ALTER TABLE google_account_api_key_link + ADD CONSTRAINT uc_google_account_api_key_link_google_oidc2 UNIQUE (google_oidc2_id); diff --git a/src/main/resources/db/migration/V5__Create_issued_api_key_table.sql .sql b/src/main/resources/db/migration/V5__Create_issued_api_key_table.sql .sql new file mode 100644 index 0000000..227d291 --- /dev/null +++ b/src/main/resources/db/migration/V5__Create_issued_api_key_table.sql .sql @@ -0,0 +1,18 @@ +CREATE TABLE issued_api_key +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + hashed_api_key VARCHAR(255) NOT NULL, + issued_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + expires_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + revoked BOOLEAN NOT NULL, + last_used_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + api_key_name VARCHAR(255) NOT NULL, + google_account_api_key_link_id BIGINT NOT NULL, + CONSTRAINT pk_issued_api_key PRIMARY KEY (id) +); + +ALTER TABLE issued_api_key + ADD CONSTRAINT uc_issued_api_key_hashedapikey UNIQUE (hashed_api_key); + +ALTER TABLE issued_api_key + ADD CONSTRAINT FK_ISSUED_API_KEY_ON_GOOGLE_ACCOUNT_API_KEY_LINK FOREIGN KEY (google_account_api_key_link_id) REFERENCES google_account_api_key_link (id); From fa77ec5395f46ae18b31917910607b5b5464529b Mon Sep 17 00:00:00 2001 From: efpcode Date: Thu, 17 Apr 2025 13:16:17 +0200 Subject: [PATCH 16/51] !(fixes) joins between tables and seeder data generation in Configfile --- .../org/storkforge/barkr/config/Config.java | 18 +++++++++++++ .../barkr/domain/entity/Account.java | 2 +- .../entity/GoogleAccountApiKeyLink.java | 2 +- ...ts_on_username_and_google_oidc2id.sql .sql | 27 ++++++++++--------- ...eate_google_account_api_key_link_table.sql | 6 ++--- 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/storkforge/barkr/config/Config.java b/src/main/java/org/storkforge/barkr/config/Config.java index 11caad0..44ed584 100644 --- a/src/main/java/org/storkforge/barkr/config/Config.java +++ b/src/main/java/org/storkforge/barkr/config/Config.java @@ -3,6 +3,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.annotation.EnableCaching; +import org.storkforge.barkr.domain.entity.GoogleAccountApiKeyLink; import org.storkforge.barkr.domain.entity.Post; import org.springframework.boot.CommandLineRunner; import org.storkforge.barkr.domain.entity.Account; @@ -31,18 +32,35 @@ CommandLineRunner accountInit(AccountRepository accountRepository, PostRepositor Account accountOne = new Account(); accountOne.setUsername("Bella Pawkins"); accountOne.setBreed("Golden Retriever"); + accountOne.setGoogleOidc2Id("1"); accountOne.setImage(null); Account accountTwo = new Account(); accountTwo.setUsername("Charlie Barkson"); accountTwo.setBreed("Siberian Husky"); + accountTwo.setGoogleOidc2Id("2"); accountTwo.setImage(null); Account accountThree = new Account(); accountThree.setUsername("Max Woofington"); accountThree.setBreed("German Shepherd"); + accountThree.setGoogleOidc2Id("3"); accountThree.setImage(null); + GoogleAccountApiKeyLink link1 = new GoogleAccountApiKeyLink(); + link1.setAccount(accountOne); + accountOne.setGoogleAccountApiKeyLink(link1); + System.out.println(accountOne.getGoogleAccountApiKeyLink()); + + GoogleAccountApiKeyLink link2 = new GoogleAccountApiKeyLink(); + link2.setAccount(accountTwo); + accountTwo.setGoogleAccountApiKeyLink(link2); + + GoogleAccountApiKeyLink link3 = new GoogleAccountApiKeyLink(); + link3.setAccount(accountThree); + accountThree.setGoogleAccountApiKeyLink(link3); + + accountRepository.saveAll(List.of(accountOne, accountTwo, accountThree)); log.info("Seeding posts... (requires users to exist first)"); diff --git a/src/main/java/org/storkforge/barkr/domain/entity/Account.java b/src/main/java/org/storkforge/barkr/domain/entity/Account.java index c53fa9e..aa789cf 100644 --- a/src/main/java/org/storkforge/barkr/domain/entity/Account.java +++ b/src/main/java/org/storkforge/barkr/domain/entity/Account.java @@ -35,7 +35,7 @@ public class Account implements Serializable { private String breed; @NotBlank - @Column(unique = true, nullable = false, updatable = false) + @Column(name = "google_oidc2id", unique = true, nullable = false, updatable = false) private String googleOidc2Id; diff --git a/src/main/java/org/storkforge/barkr/domain/entity/GoogleAccountApiKeyLink.java b/src/main/java/org/storkforge/barkr/domain/entity/GoogleAccountApiKeyLink.java index 34c093f..c96b5bf 100644 --- a/src/main/java/org/storkforge/barkr/domain/entity/GoogleAccountApiKeyLink.java +++ b/src/main/java/org/storkforge/barkr/domain/entity/GoogleAccountApiKeyLink.java @@ -19,7 +19,7 @@ public class GoogleAccountApiKeyLink implements Serializable { @NotNull @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "google_oidc2_id", referencedColumnName = "googleOidc2Id", nullable = false, unique = true, updatable = false) + @JoinColumn(name = "google_oidc2id", referencedColumnName = "google_oidc2id", nullable = false, unique = true, updatable = false) private Account account; @OneToMany(mappedBy = "googleAccountApiKeyLink", cascade = CascadeType.ALL, orphanRemoval = true) diff --git a/src/main/resources/db/migration/V3__Add_unique_constraints_on_username_and_google_oidc2id.sql .sql b/src/main/resources/db/migration/V3__Add_unique_constraints_on_username_and_google_oidc2id.sql .sql index 0097887..b68ae66 100644 --- a/src/main/resources/db/migration/V3__Add_unique_constraints_on_username_and_google_oidc2id.sql .sql +++ b/src/main/resources/db/migration/V3__Add_unique_constraints_on_username_and_google_oidc2id.sql .sql @@ -1,19 +1,20 @@ -CREATE TABLE account -( - account_id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, - username VARCHAR(255) NOT NULL, - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, - image BYTEA, - breed VARCHAR(100), - google_oidc2id VARCHAR(255) NOT NULL, - CONSTRAINT pk_account PRIMARY KEY (account_id) -); +-- Step 1: Add the column as nullable first +ALTER TABLE account + ADD google_oidc2id VARCHAR(255); + +-- Step 2: Set a default value for existing rows using the primary key +UPDATE account +SET google_oidc2id = account_id::TEXT +WHERE google_oidc2id IS NULL; +-- Step 3: Set the column as NOT NULL ALTER TABLE account - ADD CONSTRAINT uc_account_googleoidc2id UNIQUE (google_oidc2id); + ALTER COLUMN google_oidc2id SET NOT NULL; +-- Ensure the google_oidc2id column is unique ALTER TABLE account - ADD CONSTRAINT uc_account_username UNIQUE (username); + ADD CONSTRAINT uc_account_googleoidc2id UNIQUE (google_oidc2id); + --- Add unique index for performance optimization +-- Add a unique index for google_oidc2id for performance optimization CREATE UNIQUE INDEX idx_account_google_oidc2_id ON account (google_oidc2id); diff --git a/src/main/resources/db/migration/V4__Create_google_account_api_key_link_table.sql b/src/main/resources/db/migration/V4__Create_google_account_api_key_link_table.sql index 1f81404..758f73e 100644 --- a/src/main/resources/db/migration/V4__Create_google_account_api_key_link_table.sql +++ b/src/main/resources/db/migration/V4__Create_google_account_api_key_link_table.sql @@ -1,10 +1,10 @@ CREATE TABLE google_account_api_key_link ( id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, - google_oidc2_id VARCHAR(255) NOT NULL, + google_oidc2id VARCHAR(255) NOT NULL, CONSTRAINT pk_google_account_api_key_link PRIMARY KEY (id), - CONSTRAINT fk_gaakl_account_google_oidc2_id FOREIGN KEY (google_oidc2_id) REFERENCES account (google_oidc2id) + CONSTRAINT fk_gaakl_account_google_oidc2_id FOREIGN KEY (google_oidc2id) REFERENCES account (google_oidc2id) ); ALTER TABLE google_account_api_key_link - ADD CONSTRAINT uc_google_account_api_key_link_google_oidc2 UNIQUE (google_oidc2_id); + ADD CONSTRAINT uc_google_account_api_key_link_google_oidc2 UNIQUE (google_oidc2id); From 05b17b516bae8080d390ffdeca4fd69d31c48c17 Mon Sep 17 00:00:00 2001 From: efpcode Date: Thu, 17 Apr 2025 22:18:37 +0200 Subject: [PATCH 17/51] !(feature) adds new fields to account entity and fixes logout --- .../org/storkforge/barkr/config/Config.java | 7 ++++- .../barkr/config/SecurityConfig.java | 19 +++++++++++-- .../barkr/domain/IssuedApiKeyService.java | 4 +++ .../barkr/domain/entity/Account.java | 28 +++++++++++++++++-- .../entity/GoogleAccountApiKeyLink.java | 1 + .../barkr/domain/roles/BarkrRole.java | 19 +++++++++++++ src/main/resources/application.properties | 2 +- ...raints_on_username_and_google_oidc2id.sql} | 5 ---- .../V6__add_roles_column_to_account_table.sql | 8 ++++++ 9 files changed, 80 insertions(+), 13 deletions(-) create mode 100644 src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java create mode 100644 src/main/java/org/storkforge/barkr/domain/roles/BarkrRole.java rename src/main/resources/db/migration/{V3__Add_unique_constraints_on_username_and_google_oidc2id.sql .sql => V3__Add_unique_constraints_on_username_and_google_oidc2id.sql} (58%) create mode 100644 src/main/resources/db/migration/V6__add_roles_column_to_account_table.sql diff --git a/src/main/java/org/storkforge/barkr/config/Config.java b/src/main/java/org/storkforge/barkr/config/Config.java index 44ed584..d3e83d1 100644 --- a/src/main/java/org/storkforge/barkr/config/Config.java +++ b/src/main/java/org/storkforge/barkr/config/Config.java @@ -11,10 +11,13 @@ import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; +import org.storkforge.barkr.domain.roles.BarkrRole; import org.storkforge.barkr.infrastructure.persistence.PostRepository; import org.storkforge.barkr.infrastructure.persistence.AccountRepository; +import java.util.HashSet; import java.util.List; +import java.util.Set; @Configuration @Profile("!test") @@ -34,23 +37,25 @@ CommandLineRunner accountInit(AccountRepository accountRepository, PostRepositor accountOne.setBreed("Golden Retriever"); accountOne.setGoogleOidc2Id("1"); accountOne.setImage(null); + accountOne.setRoles(new HashSet<>(Set.of(BarkrRole.USER))); Account accountTwo = new Account(); accountTwo.setUsername("Charlie Barkson"); accountTwo.setBreed("Siberian Husky"); accountTwo.setGoogleOidc2Id("2"); accountTwo.setImage(null); + accountTwo.setRoles(new HashSet<>(Set.of(BarkrRole.USER))); Account accountThree = new Account(); accountThree.setUsername("Max Woofington"); accountThree.setBreed("German Shepherd"); accountThree.setGoogleOidc2Id("3"); accountThree.setImage(null); + accountThree.setRoles(new HashSet<>(Set.of(BarkrRole.USER))); GoogleAccountApiKeyLink link1 = new GoogleAccountApiKeyLink(); link1.setAccount(accountOne); accountOne.setGoogleAccountApiKeyLink(link1); - System.out.println(accountOne.getGoogleAccountApiKeyLink()); GoogleAccountApiKeyLink link2 = new GoogleAccountApiKeyLink(); link2.setAccount(accountTwo); diff --git a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java index ed02be6..b141bbc 100644 --- a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java +++ b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java @@ -31,14 +31,25 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/{username}").authenticated() .anyRequest().denyAll() - ); + ) + .logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/") + .logoutSuccessHandler((request, response, authentication) -> { + String googleLogoutUrl = "https://accounts.google.com/Logout"; + response.sendRedirect(googleLogoutUrl); + + }) + + ); + return http.build(); } @Bean @Order(1) - public SecurityFilterChain restAPIAndGraphQLFilterChain(HttpSecurity http) throws Exception{ + public SecurityFilterChain restAPIAndGraphQLFilterChain(HttpSecurity http) throws Exception { http.securityMatcher("/api/**", "/graphql") .csrf(AbstractHttpConfigurer::disable) .addFilterAfter(new ApiKeyAuthenticationFilter(), LogoutFilter.class) @@ -53,8 +64,10 @@ public SecurityFilterChain restAPIAndGraphQLFilterChain(HttpSecurity http) throw ); return http.build(); + } - } + +} diff --git a/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java b/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java new file mode 100644 index 0000000..d82ab60 --- /dev/null +++ b/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java @@ -0,0 +1,4 @@ +package org.storkforge.barkr.domain; + +public class IssuedApiKeyService { +} diff --git a/src/main/java/org/storkforge/barkr/domain/entity/Account.java b/src/main/java/org/storkforge/barkr/domain/entity/Account.java index aa789cf..a3b0b45 100644 --- a/src/main/java/org/storkforge/barkr/domain/entity/Account.java +++ b/src/main/java/org/storkforge/barkr/domain/entity/Account.java @@ -3,12 +3,14 @@ import jakarta.persistence.*; import jakarta.validation.constraints.*; import org.hibernate.proxy.HibernateProxy; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.storkforge.barkr.domain.roles.BarkrRole; import java.io.Serializable; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; +import java.util.*; +import java.util.stream.Collectors; @Entity public class Account implements Serializable { @@ -45,6 +47,26 @@ public class Account implements Serializable { @OneToOne(fetch = FetchType.LAZY, mappedBy = "account", cascade = CascadeType.ALL) private GoogleAccountApiKeyLink googleAccountApiKeyLink; + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable(name = "account_roles", joinColumns = @JoinColumn(name = "account_id")) + @Enumerated(EnumType.STRING) + @Column(name = "role") + private Set roles = new HashSet<>(); + + public Set getAuthorities() { + return roles.stream() + .map(role -> new SimpleGrantedAuthority(role.getAuthorityValue())) + .collect(Collectors.toSet()); + } + + public void setRoles(Set roles) { + this.roles = roles; + } + + public Set getRoles() { + return roles; + } + public GoogleAccountApiKeyLink getGoogleAccountApiKeyLink() { return googleAccountApiKeyLink; diff --git a/src/main/java/org/storkforge/barkr/domain/entity/GoogleAccountApiKeyLink.java b/src/main/java/org/storkforge/barkr/domain/entity/GoogleAccountApiKeyLink.java index c96b5bf..ea3e30d 100644 --- a/src/main/java/org/storkforge/barkr/domain/entity/GoogleAccountApiKeyLink.java +++ b/src/main/java/org/storkforge/barkr/domain/entity/GoogleAccountApiKeyLink.java @@ -33,6 +33,7 @@ public void setIssuedApiKeys(Set issuedApiKeys) { this.issuedApiKeys = issuedApiKeys; } + public Long getId() { return id; } diff --git a/src/main/java/org/storkforge/barkr/domain/roles/BarkrRole.java b/src/main/java/org/storkforge/barkr/domain/roles/BarkrRole.java new file mode 100644 index 0000000..025273f --- /dev/null +++ b/src/main/java/org/storkforge/barkr/domain/roles/BarkrRole.java @@ -0,0 +1,19 @@ +package org.storkforge.barkr.domain.roles; + +public enum BarkrRole { + + USER("ROLE_USER"), + PREMIUM("ROLE_PREMIUM"); + + + + private final String authorityValue; + + BarkrRole(String roleValue) { + this.authorityValue = roleValue; + } + + public String getAuthorityValue() { + return authorityValue; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b621a8..da538f0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,4 +2,4 @@ spring.application.name=barkr spring.messages.fallback-to-system-locale=false server.error.include-stacktrace=never spring.servlet.multipart.max-file-size=5MB -ai.mistral.joke-prompt=tell me a dog joke like you are a dog and dont write anything else and dont explain the joke \ No newline at end of file +ai.mistral.joke-prompt=tell me a dog joke like you are a dog and dont write anything else and dont explain the joke diff --git a/src/main/resources/db/migration/V3__Add_unique_constraints_on_username_and_google_oidc2id.sql .sql b/src/main/resources/db/migration/V3__Add_unique_constraints_on_username_and_google_oidc2id.sql similarity index 58% rename from src/main/resources/db/migration/V3__Add_unique_constraints_on_username_and_google_oidc2id.sql .sql rename to src/main/resources/db/migration/V3__Add_unique_constraints_on_username_and_google_oidc2id.sql index b68ae66..26cf4e1 100644 --- a/src/main/resources/db/migration/V3__Add_unique_constraints_on_username_and_google_oidc2id.sql .sql +++ b/src/main/resources/db/migration/V3__Add_unique_constraints_on_username_and_google_oidc2id.sql @@ -1,20 +1,15 @@ --- Step 1: Add the column as nullable first ALTER TABLE account ADD google_oidc2id VARCHAR(255); --- Step 2: Set a default value for existing rows using the primary key UPDATE account SET google_oidc2id = account_id::TEXT WHERE google_oidc2id IS NULL; --- Step 3: Set the column as NOT NULL ALTER TABLE account ALTER COLUMN google_oidc2id SET NOT NULL; --- Ensure the google_oidc2id column is unique ALTER TABLE account ADD CONSTRAINT uc_account_googleoidc2id UNIQUE (google_oidc2id); --- Add a unique index for google_oidc2id for performance optimization CREATE UNIQUE INDEX idx_account_google_oidc2_id ON account (google_oidc2id); diff --git a/src/main/resources/db/migration/V6__add_roles_column_to_account_table.sql b/src/main/resources/db/migration/V6__add_roles_column_to_account_table.sql new file mode 100644 index 0000000..bcffdd2 --- /dev/null +++ b/src/main/resources/db/migration/V6__add_roles_column_to_account_table.sql @@ -0,0 +1,8 @@ + +CREATE TABLE IF NOT EXISTS account_roles ( + account_id BIGINT NOT NULL, + role VARCHAR(255) NOT NULL DEFAULT 'ROLE_USER', + FOREIGN KEY (account_id) REFERENCES account(account_id) ON DELETE CASCADE + ); + +CREATE INDEX IF NOT EXISTS idx_account_roles_account_id ON account_roles(account_id); From a6ee85c9a436ef62aa5bb159e9b51e3922feda8d Mon Sep 17 00:00:00 2001 From: efpcode Date: Thu, 17 Apr 2025 22:57:26 +0200 Subject: [PATCH 18/51] Adds first part of user registration --- .../barkr/domain/CustomOidc2UserService.java | 53 +++++++++++++++---- .../filters/ApiKeyAuthenticationToken.java | 2 +- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java b/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java index dca5706..dac905b 100644 --- a/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java +++ b/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java @@ -1,28 +1,59 @@ package org.storkforge.barkr.domain; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.storkforge.barkr.domain.entity.Account; +import org.storkforge.barkr.domain.entity.GoogleAccountApiKeyLink; +import org.storkforge.barkr.domain.roles.BarkrRole; +import org.storkforge.barkr.infrastructure.persistence.AccountRepository; + +import java.util.HashSet; +import java.util.Set; @Service +@Transactional public class CustomOidc2UserService extends OidcUserService { + private final Logger log = LoggerFactory.getLogger(CustomOidc2UserService.class); + + private final AccountRepository accountRepository; + + public CustomOidc2UserService(AccountRepository accountRepository) { + this.accountRepository = accountRepository; + } + + + @Override public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException{ OidcUser user = super.loadUser(userRequest); - System.out.println("###########################################################################################################"); - System.out.println("The OIDC user found:"); - System.out.println(user); - System.out.println("###########################################################################################################"); - System.out.println(user.getName()); - System.out.println(user.getEmail()); - System.out.println("###########################################################################################################"); - - //TODO For Servic - // - 1. Check that user exists in db with lookup on user.getName() which is a unique value. - // - 2. If user exists retrieve user object and if not create new user entity. + String username = user.getAttributes().get("name").toString(); + String oidcId = user.getName(); + + var accountFound = accountRepository.findByUsernameEqualsIgnoreCase(username); + + if (accountFound.isPresent()) { + return user; + } + + Account account = new Account(); + GoogleAccountApiKeyLink link = new GoogleAccountApiKeyLink(); + account.setUsername(username); + account.setBreed("Snoopy"); + account.setGoogleOidc2Id(oidcId); + account.setImage(null); + account.setRoles(new HashSet<>(Set.of(BarkrRole.USER))); + link.setAccount(account); + account.setGoogleAccountApiKeyLink(link); + + accountRepository.save(account); + return user; diff --git a/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationToken.java b/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationToken.java index fd131d4..6872f27 100644 --- a/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationToken.java +++ b/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationToken.java @@ -10,7 +10,7 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { private final String apiKey; public ApiKeyAuthenticationToken(String apiKey) { - // Compatibility issues flexiable constructor not working + // Compatibility issues flexible constructor not working super(Collections.singletonList(new SimpleGrantedAuthority("ROLE_APIKEY"))); this.apiKey = apiKey; super.setAuthenticated(true); From 7b7bd06b809803225e41c414a3bce5051b923799 Mon Sep 17 00:00:00 2001 From: efpcode Date: Fri, 18 Apr 2025 01:33:57 +0200 Subject: [PATCH 19/51] Adds new find method to account + display of current user --- .../barkr/domain/AccountService.java | 4 ++ .../barkr/domain/CustomOidc2UserService.java | 5 ++- .../persistence/AccountRepository.java | 6 +++ .../barkr/web/controller/WebController.java | 38 +++++++++++++++---- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/storkforge/barkr/domain/AccountService.java b/src/main/java/org/storkforge/barkr/domain/AccountService.java index 51f2e66..53b8b6c 100644 --- a/src/main/java/org/storkforge/barkr/domain/AccountService.java +++ b/src/main/java/org/storkforge/barkr/domain/AccountService.java @@ -30,6 +30,10 @@ public AccountService(AccountRepository accountRepository) { this.accountRepository = accountRepository; } + public Optional findByGoogleOidc2Id(String googleOidc2Id) { + return accountRepository.findByGoogleOidc2Id(googleOidc2Id); + } + @Cacheable("allAccounts") public Page findAll(Pageable pageable) { log.info("Finding all accounts"); diff --git a/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java b/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java index dac905b..2b19a09 100644 --- a/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java +++ b/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java @@ -28,8 +28,6 @@ public CustomOidc2UserService(AccountRepository accountRepository) { } - - @Override public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException{ OidcUser user = super.loadUser(userRequest); @@ -50,8 +48,10 @@ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2Authenticatio account.setImage(null); account.setRoles(new HashSet<>(Set.of(BarkrRole.USER))); link.setAccount(account); + log.info("New record added to database"); account.setGoogleAccountApiKeyLink(link); + accountRepository.save(account); @@ -59,4 +59,5 @@ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2Authenticatio } + } diff --git a/src/main/java/org/storkforge/barkr/infrastructure/persistence/AccountRepository.java b/src/main/java/org/storkforge/barkr/infrastructure/persistence/AccountRepository.java index 65878fe..a6abcfa 100644 --- a/src/main/java/org/storkforge/barkr/infrastructure/persistence/AccountRepository.java +++ b/src/main/java/org/storkforge/barkr/infrastructure/persistence/AccountRepository.java @@ -1,5 +1,6 @@ package org.storkforge.barkr.infrastructure.persistence; +import jakarta.validation.constraints.NotBlank; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.storkforge.barkr.domain.entity.Account; @@ -11,4 +12,9 @@ public interface AccountRepository extends JpaRepository { @Query("SELECT a.image FROM Account a WHERE a.id = :id") Optional getAccountImage(Long id); + + + Optional findByGoogleOidc2Id(@NotBlank String googleOidc2Id); + + } diff --git a/src/main/java/org/storkforge/barkr/web/controller/WebController.java b/src/main/java/org/storkforge/barkr/web/controller/WebController.java index 2e71e50..ec63dfa 100644 --- a/src/main/java/org/storkforge/barkr/web/controller/WebController.java +++ b/src/main/java/org/storkforge/barkr/web/controller/WebController.java @@ -10,6 +10,9 @@ import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.ui.Model; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; @@ -24,6 +27,7 @@ import java.io.IOException; import java.io.InputStream; +import java.security.Principal; @Controller @RequestMapping("/") @@ -40,18 +44,28 @@ public WebController(PostService postService, AccountService accountService, Dog } @GetMapping("/") - public String index(Model model, @PageableDefault Pageable pageable) { + public String index(Model model, @PageableDefault Pageable pageable, @AuthenticationPrincipal OidcUser user) { + long id = 1L; + + Principal context = SecurityContextHolder.getContext().getAuthentication(); + + if (!context.getName().equals("anonymousUser")) { + + var currentUser = accountService.findByGoogleOidc2Id(user.getName()); + id = currentUser.isEmpty() ? 1L : currentUser.get().getId(); + } + + model.addAttribute("posts", postService.findAll(pageable)); - model.addAttribute("createPostDto", new CreatePost("", 1L)); + model.addAttribute("createPostDto", new CreatePost("", id)); model.addAttribute("fact", dogFactService.getDogFact()); - // TODO: Change this to the actual account once security is in place - model.addAttribute("account", accountService.findById(1L)); + model.addAttribute("account", accountService.findById(id)); return "index"; } @GetMapping("/{username}") - public String user(@PathVariable("username") @NotBlank String username, Model model, RedirectAttributes redirectAttributes, @PageableDefault Pageable pageable) { + public String user(@PathVariable("username") @NotBlank String username, Model model, RedirectAttributes redirectAttributes, @PageableDefault Pageable pageable, @AuthenticationPrincipal OidcUser user) { ResponseAccount queryAccount; try { queryAccount = accountService.findByUsername(username); @@ -61,11 +75,20 @@ public String user(@PathVariable("username") @NotBlank String username, Model mo return "redirect:/"; } + long id = 1L; + + Principal context = SecurityContextHolder.getContext().getAuthentication(); + + if (!context.getName().equals("anonymousUser")) { + + var currentUser = accountService.findByGoogleOidc2Id(user.getName()); + id = currentUser.isEmpty() ? 1L : currentUser.get().getId(); + } + model.addAttribute("accountPosts", postService.findByUsername(username, pageable)); model.addAttribute("queryAccount", queryAccount); model.addAttribute("fact", dogFactService.getDogFact()); - // TODO: Change this to the actual account once security is in place - model.addAttribute("account", accountService.findById(1L)); + model.addAttribute("account", accountService.findById(id)); return "profile"; } @@ -137,4 +160,3 @@ public String uploadImage(@PathVariable @Positive @NotNull Long id, @RequestPara return "redirect:/"; } } - From 8fd974ad610e8fd6f1f7ef38036e3fd212782eb4 Mon Sep 17 00:00:00 2001 From: efpcode Date: Fri, 18 Apr 2025 21:32:40 +0200 Subject: [PATCH 20/51] (fix) Adds proper logout + foundation for apikey generation --- .../storkforge/barkr/dto/apiKeyDto/CreateApiKey.java | 4 ++++ .../barkr/dto/apiKeyDto/GenerateApiKeyRequest.java | 11 +++++++++++ .../barkr/dto/apiKeyDto/ResponseApiKey.java | 4 ++++ .../barkr/dto/apiKeyDto/ResponseApiKeyList.java | 4 ++++ .../barkr/dto/apiKeyDto/ResponseApiKeyOnce.java | 4 ++++ .../storkforge/barkr/dto/apiKeyDto/UpdateApiKey.java | 4 ++++ .../GoogleAccountApiKeyLinkRepository.java | 4 ++++ .../persistence/IssuedApiKeyRepository.java | 4 ++++ .../org/storkforge/barkr/mapper/ApiKeyMapper.java | 4 ++++ .../resources/templates/fragments/barkr-logout.html | 10 ++++++++++ 10 files changed, 53 insertions(+) create mode 100644 src/main/java/org/storkforge/barkr/dto/apiKeyDto/CreateApiKey.java create mode 100644 src/main/java/org/storkforge/barkr/dto/apiKeyDto/GenerateApiKeyRequest.java create mode 100644 src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKey.java create mode 100644 src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKeyList.java create mode 100644 src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKeyOnce.java create mode 100644 src/main/java/org/storkforge/barkr/dto/apiKeyDto/UpdateApiKey.java create mode 100644 src/main/java/org/storkforge/barkr/infrastructure/persistence/GoogleAccountApiKeyLinkRepository.java create mode 100644 src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java create mode 100644 src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java create mode 100644 src/main/resources/templates/fragments/barkr-logout.html diff --git a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/CreateApiKey.java b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/CreateApiKey.java new file mode 100644 index 0000000..88f0d5d --- /dev/null +++ b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/CreateApiKey.java @@ -0,0 +1,4 @@ +package org.storkforge.barkr.dto.apiKeyDto; + +public record CreateApiKey() { +} diff --git a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/GenerateApiKeyRequest.java b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/GenerateApiKeyRequest.java new file mode 100644 index 0000000..09f9bfa --- /dev/null +++ b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/GenerateApiKeyRequest.java @@ -0,0 +1,11 @@ +package org.storkforge.barkr.dto.apiKeyDto; + +import jakarta.validation.constraints.NotBlank; + +import java.io.Serializable; +import java.time.LocalDateTime; + +public record CreateApiKey( + @NotBlank String apiKeyName , + LocalDateTime expiresAt) implements Serializable { +} diff --git a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKey.java b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKey.java new file mode 100644 index 0000000..283f063 --- /dev/null +++ b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKey.java @@ -0,0 +1,4 @@ +package org.storkforge.barkr.dto.apikeyDto; + +public record ResponseApiKey() { +} diff --git a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKeyList.java b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKeyList.java new file mode 100644 index 0000000..d3a092b --- /dev/null +++ b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKeyList.java @@ -0,0 +1,4 @@ +package org.storkforge.barkr.dto.apikeyDto; + +public record ResponseApiKeyList() { +} diff --git a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKeyOnce.java b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKeyOnce.java new file mode 100644 index 0000000..5d59e8a --- /dev/null +++ b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKeyOnce.java @@ -0,0 +1,4 @@ +package org.storkforge.barkr.dto.apikeyDto; + +public record ResponseApiKeyOnce() { +} diff --git a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/UpdateApiKey.java b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/UpdateApiKey.java new file mode 100644 index 0000000..835c9da --- /dev/null +++ b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/UpdateApiKey.java @@ -0,0 +1,4 @@ +package org.storkforge.barkr.dto.apikeyDto; + +public record UpdateApiKey() { +} diff --git a/src/main/java/org/storkforge/barkr/infrastructure/persistence/GoogleAccountApiKeyLinkRepository.java b/src/main/java/org/storkforge/barkr/infrastructure/persistence/GoogleAccountApiKeyLinkRepository.java new file mode 100644 index 0000000..47b776b --- /dev/null +++ b/src/main/java/org/storkforge/barkr/infrastructure/persistence/GoogleAccountApiKeyLinkRepository.java @@ -0,0 +1,4 @@ +package org.storkforge.barkr.infrastructure.persistence; + +public class GoogleAccountApiKeyLinkRepository { +} diff --git a/src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java b/src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java new file mode 100644 index 0000000..1741b8a --- /dev/null +++ b/src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java @@ -0,0 +1,4 @@ +package org.storkforge.barkr.infrastructure.persistence; + +public class IssuedApiKeyRepository { +} diff --git a/src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java b/src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java new file mode 100644 index 0000000..141e033 --- /dev/null +++ b/src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java @@ -0,0 +1,4 @@ +package org.storkforge.barkr.mapper; + +public class ApiKeyMapper { +} diff --git a/src/main/resources/templates/fragments/barkr-logout.html b/src/main/resources/templates/fragments/barkr-logout.html new file mode 100644 index 0000000..6b6499c --- /dev/null +++ b/src/main/resources/templates/fragments/barkr-logout.html @@ -0,0 +1,10 @@ + + + + + $Title$ + + +$END$ + + From c54730e3ea4b00355da042ee88a85e4cc92c3ce5 Mon Sep 17 00:00:00 2001 From: efpcode Date: Fri, 18 Apr 2025 21:33:52 +0200 Subject: [PATCH 21/51] fix foundation for api key and logout --- .../barkr/config/SecurityConfig.java | 13 +++--- .../barkr/domain/CustomOidc2UserService.java | 1 + .../barkr/domain/IssuedApiKeyService.java | 40 +++++++++++++++++++ .../barkr/domain/entity/IssuedApiKey.java | 4 +- .../barkr/dto/apiKeyDto/CreateApiKey.java | 12 +++++- .../dto/apiKeyDto/GenerateApiKeyRequest.java | 2 +- .../barkr/dto/apiKeyDto/ResponseApiKey.java | 19 ++++++++- .../dto/apiKeyDto/ResponseApiKeyList.java | 6 ++- .../dto/apiKeyDto/ResponseApiKeyOnce.java | 11 ++++- .../barkr/dto/apiKeyDto/UpdateApiKey.java | 12 +++++- .../GoogleAccountApiKeyLinkRepository.java | 9 ++++- .../persistence/IssuedApiKeyRepository.java | 10 ++++- .../storkforge/barkr/mapper/ApiKeyMapper.java | 25 ++++++++++++ .../barkr/web/controller/WebController.java | 6 +++ .../templates/fragments/barkr-logout.html | 19 +++++++-- .../resources/templates/fragments/header.html | 4 +- .../templates/fragments/sidebar.html | 4 +- 17 files changed, 170 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java index b141bbc..35b1288 100644 --- a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java +++ b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java @@ -24,6 +24,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .authorizeHttpRequests(authorize -> authorize .requestMatchers("/","/login", "/error", "/css/**", "/js/**", "/images/**").permitAll() .requestMatchers("/post/load").permitAll() + .requestMatchers("/barkr/logout").permitAll() .requestMatchers( "/account/{id}/image").permitAll() .requestMatchers("/ai/generate").authenticated() .requestMatchers("/post/add").authenticated() @@ -34,12 +35,12 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti ) .logout(logout -> logout .logoutUrl("/logout") - .logoutSuccessUrl("/") - .logoutSuccessHandler((request, response, authentication) -> { - String googleLogoutUrl = "https://accounts.google.com/Logout"; - response.sendRedirect(googleLogoutUrl); - - }) + .logoutSuccessUrl("/barkr/logout") +// .logoutSuccessHandler((request, response, authentication) -> { +// String googleLogoutUrl = "https://accounts.google.com/Logout"; +// response.sendRedirect(googleLogoutUrl); +// +// }) ); diff --git a/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java b/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java index 2b19a09..143a415 100644 --- a/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java +++ b/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java @@ -2,6 +2,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; diff --git a/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java b/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java index d82ab60..9c88013 100644 --- a/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java +++ b/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java @@ -1,4 +1,44 @@ package org.storkforge.barkr.domain; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.storkforge.barkr.domain.entity.IssuedApiKey; +import org.storkforge.barkr.infrastructure.persistence.GoogleAccountApiKeyLinkRepository; +import org.storkforge.barkr.infrastructure.persistence.IssuedApiKeyRepository; + +import java.util.Optional; + +@Service +@Transactional public class IssuedApiKeyService { + + private final Logger log = LoggerFactory.getLogger(IssuedApiKeyService.class); + + private final IssuedApiKeyRepository issuedApiKeyRepository; + private final GoogleAccountApiKeyLinkRepository googleAccountApiKeyLinkRepository; + + + public IssuedApiKeyService(IssuedApiKeyRepository issuedApiKeyRepository, GoogleAccountApiKeyLinkRepository googleAccountApiKeyLinkRepository) { + this.issuedApiKeyRepository = issuedApiKeyRepository; + this.googleAccountApiKeyLinkRepository = googleAccountApiKeyLinkRepository; + + } + + + public Optional issuedApiKeyExists(String hashedApiKey) { + log.info("Checking if issued api key exists"); + return issuedApiKeyRepository.findByHashedApiKey(hashedApiKey); + } + + + + + public void save(IssuedApiKey issuedApiKey) { + issuedApiKeyRepository.save(issuedApiKey); + } + + + } diff --git a/src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java b/src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java index 3dda712..19e7441 100644 --- a/src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java +++ b/src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java @@ -16,7 +16,7 @@ public class IssuedApiKey implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(nullable = false, updatable = false) + @Column(nullable = false, updatable = false, unique = true) private Long id; @NotBlank @@ -38,7 +38,7 @@ public class IssuedApiKey implements Serializable { @Column(nullable = false) @NotNull - @FutureOrPresent + @PastOrPresent private LocalDateTime lastUsedAt; @Column(nullable = false) diff --git a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/CreateApiKey.java b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/CreateApiKey.java index 88f0d5d..faa9b3d 100644 --- a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/CreateApiKey.java +++ b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/CreateApiKey.java @@ -1,4 +1,14 @@ package org.storkforge.barkr.dto.apiKeyDto; -public record CreateApiKey() { +import java.io.Serializable; +import java.time.LocalDateTime; + +public record CreateApiKey( + String hashedApiKey, + LocalDateTime issuedAt, + LocalDateTime expiresAt, + Boolean revoked, + LocalDateTime lastUsedAt, + String apiKeyName +) implements Serializable { } diff --git a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/GenerateApiKeyRequest.java b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/GenerateApiKeyRequest.java index 09f9bfa..964d4db 100644 --- a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/GenerateApiKeyRequest.java +++ b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/GenerateApiKeyRequest.java @@ -5,7 +5,7 @@ import java.io.Serializable; import java.time.LocalDateTime; -public record CreateApiKey( +public record GenerateApiKeyRequest( @NotBlank String apiKeyName , LocalDateTime expiresAt) implements Serializable { } diff --git a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKey.java b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKey.java index 283f063..5ea9f96 100644 --- a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKey.java +++ b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKey.java @@ -1,4 +1,19 @@ -package org.storkforge.barkr.dto.apikeyDto; +package org.storkforge.barkr.dto.apiKeyDto; -public record ResponseApiKey() { +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PastOrPresent; + +import java.io.Serializable; +import java.time.LocalDateTime; + +public record ResponseApiKey( + @PastOrPresent LocalDateTime issuedAt, + @FutureOrPresent LocalDateTime expiresAt, + @FutureOrPresent LocalDateTime lastUsedAt, + @NotBlank String apiKeyName, + @NotNull Boolean revoked + + ) implements Serializable { } diff --git a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKeyList.java b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKeyList.java index d3a092b..347d74b 100644 --- a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKeyList.java +++ b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKeyList.java @@ -1,4 +1,6 @@ -package org.storkforge.barkr.dto.apikeyDto; +package org.storkforge.barkr.dto.apiKeyDto; -public record ResponseApiKeyList() { +import java.util.List; + +public record ResponseApiKeyList(List apiKeys) { } diff --git a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKeyOnce.java b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKeyOnce.java index 5d59e8a..832a7b9 100644 --- a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKeyOnce.java +++ b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKeyOnce.java @@ -1,4 +1,11 @@ -package org.storkforge.barkr.dto.apikeyDto; +package org.storkforge.barkr.dto.apiKeyDto; -public record ResponseApiKeyOnce() { +import jakarta.validation.constraints.NotBlank; + +import java.io.Serializable; + +public record ResponseApiKeyOnce( + @NotBlank String key, + @NotBlank String value, + @NotBlank String message) implements Serializable { } diff --git a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/UpdateApiKey.java b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/UpdateApiKey.java index 835c9da..f44ca5a 100644 --- a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/UpdateApiKey.java +++ b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/UpdateApiKey.java @@ -1,4 +1,12 @@ -package org.storkforge.barkr.dto.apikeyDto; +package org.storkforge.barkr.dto.apiKeyDto; -public record UpdateApiKey() { +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.io.Serializable; + +public record UpdateApiKey( + @NotBlank String apiKeyTarget, + @NotBlank String apiKeyName, + @NotNull Boolean revoke) implements Serializable { } diff --git a/src/main/java/org/storkforge/barkr/infrastructure/persistence/GoogleAccountApiKeyLinkRepository.java b/src/main/java/org/storkforge/barkr/infrastructure/persistence/GoogleAccountApiKeyLinkRepository.java index 47b776b..545bc9e 100644 --- a/src/main/java/org/storkforge/barkr/infrastructure/persistence/GoogleAccountApiKeyLinkRepository.java +++ b/src/main/java/org/storkforge/barkr/infrastructure/persistence/GoogleAccountApiKeyLinkRepository.java @@ -1,4 +1,11 @@ package org.storkforge.barkr.infrastructure.persistence; -public class GoogleAccountApiKeyLinkRepository { +import org.springframework.data.jpa.repository.JpaRepository; +import org.storkforge.barkr.domain.entity.GoogleAccountApiKeyLink; + +import java.util.Optional; + +public interface GoogleAccountApiKeyLinkRepository extends JpaRepository { + @Override + Optional findById(Long id); } diff --git a/src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java b/src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java index 1741b8a..a88a94e 100644 --- a/src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java +++ b/src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java @@ -1,4 +1,12 @@ package org.storkforge.barkr.infrastructure.persistence; +import org.springframework.data.jpa.repository.JpaRepository; +import org.storkforge.barkr.domain.entity.IssuedApiKey; -public class IssuedApiKeyRepository { +import java.util.Optional; + +public interface IssuedApiKeyRepository extends JpaRepository { + @Override + Optional findById(Long id); + + Optional findByHashedApiKey(String hashedApiKey); } diff --git a/src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java b/src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java index 141e033..8d8fc01 100644 --- a/src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java +++ b/src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java @@ -1,4 +1,29 @@ package org.storkforge.barkr.mapper; +import org.storkforge.barkr.dto.apiKeyDto.GenerateApiKeyRequest; + +import java.time.LocalDateTime; + public class ApiKeyMapper { + + private ApiKeyMapper() { + } + + public static GenerateApiKeyRequest normalizeExpiresAt(GenerateApiKeyRequest createApiKey) { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime inputDate = createApiKey.expiresAt(); + + if (inputDate != null && inputDate.isBefore(now)) { + inputDate = null; + } + + if (inputDate != null && inputDate.isAfter(now.plusDays(15))) { + inputDate = LocalDateTime.now().plusDays(15); + } + + + + return new GenerateApiKeyRequest(createApiKey.apiKeyName(), inputDate); + + } } diff --git a/src/main/java/org/storkforge/barkr/web/controller/WebController.java b/src/main/java/org/storkforge/barkr/web/controller/WebController.java index ec63dfa..379a644 100644 --- a/src/main/java/org/storkforge/barkr/web/controller/WebController.java +++ b/src/main/java/org/storkforge/barkr/web/controller/WebController.java @@ -93,6 +93,12 @@ public String user(@PathVariable("username") @NotBlank String username, Model mo return "profile"; } + @GetMapping("/barkr/logout") + public String barkrLogout(){ + SecurityContextHolder.clearContext(); + return "fragments/barkr-logout"; + } + @GetMapping("/post/load") public String loadPosts(@RequestParam("page") int page, Model model, @PageableDefault Pageable pageable) { pageable = PageRequest.of(page, pageable.getPageSize()); diff --git a/src/main/resources/templates/fragments/barkr-logout.html b/src/main/resources/templates/fragments/barkr-logout.html index 6b6499c..feda936 100644 --- a/src/main/resources/templates/fragments/barkr-logout.html +++ b/src/main/resources/templates/fragments/barkr-logout.html @@ -1,10 +1,23 @@ - - $Title$ + + Title Logging Out of Barkr -$END$ +

Hope to see you!

+

You have been logged out of Barkr App via Google.

+ + + diff --git a/src/main/resources/templates/fragments/header.html b/src/main/resources/templates/fragments/header.html index 6566788..152fdac 100644 --- a/src/main/resources/templates/fragments/header.html +++ b/src/main/resources/templates/fragments/header.html @@ -7,8 +7,8 @@

Barkr

- - + +
+
+ +
+ + +
+ + + +
+ +
+ +
+ + diff --git a/src/main/resources/templates/apikeys/mykeys.html b/src/main/resources/templates/apikeys/mykeys.html new file mode 100644 index 0000000..7c77a97 --- /dev/null +++ b/src/main/resources/templates/apikeys/mykeys.html @@ -0,0 +1,51 @@ + + + + API Keys + + + +

API Keys

+ + + + + + + + + + + + + + + + + + + + + +
API Key NameIssued AtExpires AtLast Used AtRevokedActions
test2025-04-20T15:442025-04-20T15:492025-04-20T15:44false +
+ + +
+
+ + diff --git a/src/main/resources/templates/apikeys/result.html b/src/main/resources/templates/apikeys/result.html new file mode 100644 index 0000000..eb46009 --- /dev/null +++ b/src/main/resources/templates/apikeys/result.html @@ -0,0 +1,33 @@ + + + + API Key Created + + + + + +
+

Your API Key

+ +
+ + +
+ +
+ + +
+ +

Please store these credentials safely.

+ +
+ + Done +
+
+ + + + diff --git a/src/main/resources/templates/fragments/barkr-logout.html b/src/main/resources/templates/fragments/barkr-logout.html index feda936..398fa9b 100644 --- a/src/main/resources/templates/fragments/barkr-logout.html +++ b/src/main/resources/templates/fragments/barkr-logout.html @@ -2,6 +2,7 @@ + Title Logging Out of Barkr diff --git a/src/main/resources/templates/profile.html b/src/main/resources/templates/profile.html index 3606ddb..5b3670b 100644 --- a/src/main/resources/templates/profile.html +++ b/src/main/resources/templates/profile.html @@ -18,9 +18,15 @@
Profile Picture -
+
+
+ +
+ +

Unknown account

@@ -75,4 +81,4 @@

Edit Profile

- \ No newline at end of file + From 71a3d69b96fc88b4a80af0e621eb725220128b39 Mon Sep 17 00:00:00 2001 From: efpcode Date: Mon, 21 Apr 2025 01:14:14 +0200 Subject: [PATCH 23/51] Adds revoke buttons adds new column for reference id to apikey table --- .../api/controller/ApiKeyController.java | 25 +++- .../barkr/config/SecurityConfig.java | 1 + .../barkr/domain/IssuedApiKeyService.java | 32 ++++- .../barkr/domain/entity/IssuedApiKey.java | 16 +++ .../barkr/dto/apiKeyDto/CreateApiKey.java | 5 +- .../barkr/dto/apiKeyDto/ResponseApiKey.java | 11 +- .../barkr/dto/apiKeyDto/UpdateApiKey.java | 8 +- .../exceptions/IssuedApiKeyNotFound.java | 14 ++ .../persistence/IssuedApiKeyRepository.java | 15 ++- .../storkforge/barkr/mapper/ApiKeyMapper.java | 37 +++++- ..._adds_referenceID_uuid_to_apikey_table.sql | 15 +++ src/main/resources/messages_en.properties | 5 +- src/main/resources/messages_sv.properties | 5 +- src/main/resources/static/css/styles.css | 5 + .../resources/static/js/update-apikey-name.js | 10 ++ .../resources/templates/apikeys/mykeys.html | 124 +++++++++++++----- .../templates/fragments/sidebar.html | 4 + 17 files changed, 274 insertions(+), 58 deletions(-) create mode 100644 src/main/java/org/storkforge/barkr/exceptions/IssuedApiKeyNotFound.java create mode 100644 src/main/resources/db/migration/V7__adds_referenceID_uuid_to_apikey_table.sql create mode 100644 src/main/resources/static/js/update-apikey-name.js diff --git a/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java b/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java index f15bd8d..f97383a 100644 --- a/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java +++ b/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java @@ -2,6 +2,8 @@ import org.springframework.format.annotation.DateTimeFormat; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; @@ -9,6 +11,8 @@ import org.storkforge.barkr.domain.IssuedApiKeyService; import org.storkforge.barkr.dto.apiKeyDto.GenerateApiKeyRequest; import org.storkforge.barkr.dto.apiKeyDto.ResponseApiKeyOnce; +import org.storkforge.barkr.dto.apiKeyDto.UpdateApiKey; +import org.storkforge.barkr.infrastructure.persistence.AccountRepository; import org.storkforge.barkr.mapper.ApiKeyMapper; import java.security.InvalidKeyException; @@ -16,15 +20,18 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; +import java.util.UUID; @Controller @RequestMapping("/apikeys") public class ApiKeyController { private final IssuedApiKeyService issuedApiKeyService; + private final AccountRepository accountRepository; - public ApiKeyController(IssuedApiKeyService issuedApiKeyService) { + public ApiKeyController(IssuedApiKeyService issuedApiKeyService, AccountRepository accountRepository) { this.issuedApiKeyService = issuedApiKeyService; + this.accountRepository = accountRepository; } @GetMapping("/apikeyform") @@ -80,16 +87,26 @@ public String generateApiKey( } @GetMapping("/mykeys") - public String myKeys(Model model) { + public String myKeys(Model model , @AuthenticationPrincipal OidcUser user) { + var currentUser = accountRepository.findByGoogleOidc2Id(user.getName()); var keys = issuedApiKeyService.allApiKeys(); model.addAttribute("keys", keys.apiKeys()); + model.addAttribute("account", currentUser.get()); return "apikeys/mykeys"; } @PostMapping("/mykeys/revoke") - public String revokeKey(@RequestParam String apiKeyName) { - System.out.println("Pressed button"); + public String revokeKey(@RequestParam String referenceId) { + var update = new UpdateApiKey(UUID.fromString(referenceId),null, true); + issuedApiKeyService.updateApiKey(update); + return "redirect:/apikeys/mykeys"; + } + + @PostMapping("mykeys/nameupdate") + public String nameUpdate(@RequestParam String apiKeyName, @RequestParam String referenceId) { + var update = new UpdateApiKey(UUID.fromString(referenceId), apiKeyName, false); + issuedApiKeyService.updateApiKey(update); return "redirect:/apikeys/mykeys"; } diff --git a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java index ddbc703..508f645 100644 --- a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java +++ b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java @@ -35,6 +35,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/apikeys/result").authenticated() .requestMatchers("/apikeys/mykeys").authenticated() .requestMatchers("/apikeys/mykeys/revoke").authenticated() + .requestMatchers("/apikeys/mykeys/nameupdate").authenticated() .anyRequest().denyAll() diff --git a/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java b/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java index a454672..2f2c33c 100644 --- a/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java +++ b/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java @@ -12,6 +12,8 @@ import org.storkforge.barkr.dto.apiKeyDto.CreateApiKey; import org.storkforge.barkr.dto.apiKeyDto.GenerateApiKeyRequest; import org.storkforge.barkr.dto.apiKeyDto.ResponseApiKeyList; +import org.storkforge.barkr.dto.apiKeyDto.UpdateApiKey; +import org.storkforge.barkr.exceptions.IssuedApiKeyNotFound; import org.storkforge.barkr.infrastructure.persistence.AccountRepository; import org.storkforge.barkr.infrastructure.persistence.GoogleAccountApiKeyLinkRepository; import org.storkforge.barkr.infrastructure.persistence.IssuedApiKeyRepository; @@ -25,6 +27,7 @@ import java.time.LocalDateTime; import java.util.Base64; import java.util.Optional; +import java.util.UUID; @Service @Transactional @@ -72,7 +75,8 @@ public void apiKeyGenerate(GenerateApiKeyRequest request, String hashedApiKey) { false, issueDate, request.apiKeyName(), - link.get() + link.get(), + generateUuid() ); log.info("Adding new api key to account"); @@ -89,6 +93,17 @@ public String generateRawApiKey() { } + public UUID generateUuid() { + Optional uuidFound; + UUID uuid; + do { + uuid = UUID.randomUUID(); + uuidFound = issuedApiKeyRepository.findByReferenceId(uuid); + }while(uuidFound.isPresent()); + + return uuid; + } + public String hashedApiKey(String rawApiKey) throws NoSuchAlgorithmException, InvalidKeyException { @@ -121,9 +136,13 @@ public ResponseApiKeyList allApiKeys() { SecurityContext securityContext = SecurityContextHolder.getContext(); Authentication authentication = securityContext.getAuthentication(); var googleOidc2id = authentication.getName(); + + var recordsRemoved = issuedApiKeyRepository.revokeExpiredKeys(LocalDateTime.now()); + log.info("Removed " + recordsRemoved + " api keys"); + var account = accountRepository.findByGoogleOidc2Id(googleOidc2id); var link = googleAccountApiKeyLinkRepository.findByAccount(account.get()); - var apiKeys = issuedApiKeyRepository.findByGoogleAccountApiKeyLink(link.get()); + var apiKeys = issuedApiKeyRepository.findByGoogleAccountApiKeyLinkOrderByIssuedAtDesc(link.get()); var apiKeyResponse = apiKeys.stream().map(ApiKeyMapper::mapToResponse).toList(); return new ResponseApiKeyList(apiKeyResponse); @@ -131,6 +150,15 @@ public ResponseApiKeyList allApiKeys() { } + public void updateApiKey(UpdateApiKey updateApiKey) { + var apikey = issuedApiKeyRepository.findByReferenceId(updateApiKey.referenceId()).orElseThrow(() -> new IssuedApiKeyNotFound("No api key was found")); + var updateApikey = ApiKeyMapper.updateIssuedApiKey(apikey, updateApiKey); + issuedApiKeyRepository.save(updateApikey); + + } + + + } diff --git a/src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java b/src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java index 19e7441..50ef358 100644 --- a/src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java +++ b/src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java @@ -10,6 +10,7 @@ import java.io.Serializable; import java.time.LocalDateTime; import java.util.Objects; +import java.util.UUID; @Entity @Table(name = "issued_api_key") @@ -45,6 +46,10 @@ public class IssuedApiKey implements Serializable { @NotBlank private String apiKeyName; + @Column(nullable = false, updatable = false, unique = true) + @NotNull + private UUID referenceId; + @NotNull @ManyToOne @JoinColumn(name = "google_account_api_key_link_id") @@ -66,6 +71,8 @@ protected void onCreate() { if(lastUsedAt == null) lastUsedAt = issuedAt; } + + public Long getId() { return id; } @@ -123,6 +130,14 @@ public void setApiKeyName(String apiKeyName) { } + public UUID getReferenceId() { + return referenceId; + } + + public void setReferenceId(UUID referenceId) { + this.referenceId = referenceId; + } + @Override public final boolean equals(Object o) { if (this == o) return true; @@ -150,6 +165,7 @@ public String toString() { "revoked = " + revoked + ", " + "lastUsedAt = " + lastUsedAt + ", " + "apiKeyName = " + apiKeyName + ", " + + "referenceId = " + referenceId + ", " + "googleAccountApiKeyLink = " + googleAccountApiKeyLink + ")"; } } diff --git a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/CreateApiKey.java b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/CreateApiKey.java index 18de509..5156028 100644 --- a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/CreateApiKey.java +++ b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/CreateApiKey.java @@ -8,6 +8,7 @@ import java.io.Serializable; import java.time.LocalDateTime; +import java.util.UUID; public record CreateApiKey( @NotBlank String hashedApiKey, @@ -16,6 +17,6 @@ public record CreateApiKey( @NotNull Boolean revoked, @PastOrPresent LocalDateTime lastUsedAt, @NotBlank String apiKeyName, - @NotNull GoogleAccountApiKeyLink googleAccountApiKeyLink -) implements Serializable { + @NotNull GoogleAccountApiKeyLink googleAccountApiKeyLink, + @NotBlank UUID referenceId) implements Serializable { } diff --git a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKey.java b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKey.java index 5ea9f96..96c349f 100644 --- a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKey.java +++ b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKey.java @@ -6,14 +6,15 @@ import jakarta.validation.constraints.PastOrPresent; import java.io.Serializable; -import java.time.LocalDateTime; +import java.util.UUID; public record ResponseApiKey( - @PastOrPresent LocalDateTime issuedAt, - @FutureOrPresent LocalDateTime expiresAt, - @FutureOrPresent LocalDateTime lastUsedAt, + @NotBlank String issuedAt, + @NotBlank String expiresAt, + @NotBlank String lastUsedAt, @NotBlank String apiKeyName, - @NotNull Boolean revoked + @NotNull Boolean revoked, + @NotNull UUID referenceId ) implements Serializable { } diff --git a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/UpdateApiKey.java b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/UpdateApiKey.java index f44ca5a..092c8cd 100644 --- a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/UpdateApiKey.java +++ b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/UpdateApiKey.java @@ -1,12 +1,12 @@ package org.storkforge.barkr.dto.apiKeyDto; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.io.Serializable; +import java.util.UUID; public record UpdateApiKey( - @NotBlank String apiKeyTarget, - @NotBlank String apiKeyName, - @NotNull Boolean revoke) implements Serializable { + @NotNull UUID referenceId, + String apiKeyName, + Boolean revoke) implements Serializable { } diff --git a/src/main/java/org/storkforge/barkr/exceptions/IssuedApiKeyNotFound.java b/src/main/java/org/storkforge/barkr/exceptions/IssuedApiKeyNotFound.java new file mode 100644 index 0000000..f1af204 --- /dev/null +++ b/src/main/java/org/storkforge/barkr/exceptions/IssuedApiKeyNotFound.java @@ -0,0 +1,14 @@ +package org.storkforge.barkr.exceptions; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class IssuedApiKeyNotFound extends RuntimeException { + public IssuedApiKeyNotFound() { + super(); + } + public IssuedApiKeyNotFound(String message) { + super(message); + } +} diff --git a/src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java b/src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java index 191d443..9667f79 100644 --- a/src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java +++ b/src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java @@ -1,11 +1,16 @@ package org.storkforge.barkr.infrastructure.persistence; import jakarta.validation.constraints.NotNull; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.storkforge.barkr.domain.entity.GoogleAccountApiKeyLink; import org.storkforge.barkr.domain.entity.IssuedApiKey; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; +import java.util.UUID; public interface IssuedApiKeyRepository extends JpaRepository { @Override @@ -13,6 +18,12 @@ public interface IssuedApiKeyRepository extends JpaRepository findByHashedApiKey(String hashedApiKey); - List findByGoogleAccountApiKeyLink(@NotNull GoogleAccountApiKeyLink googleAccountApiKeyLink); - + List findByGoogleAccountApiKeyLinkOrderByIssuedAtDesc(@NotNull GoogleAccountApiKeyLink googleAccountApiKeyLink); + + Optional findByReferenceId(@NotNull UUID referenceId); + + @Modifying + @Query("UPDATE IssuedApiKey k SET k.revoked = true WHERE k.expiresAt <= :now AND k.revoked = false") + int revokeExpiredKeys(@Param("now") LocalDateTime now); + } diff --git a/src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java b/src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java index 5c060c0..f63bab5 100644 --- a/src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java +++ b/src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java @@ -4,8 +4,10 @@ import org.storkforge.barkr.dto.apiKeyDto.CreateApiKey; import org.storkforge.barkr.dto.apiKeyDto.GenerateApiKeyRequest; import org.storkforge.barkr.dto.apiKeyDto.ResponseApiKey; +import org.storkforge.barkr.dto.apiKeyDto.UpdateApiKey; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; public class ApiKeyMapper { @@ -26,6 +28,7 @@ public static IssuedApiKey mapToEntity(CreateApiKey createApiKey) { issuedApiKey.setRevoked(createApiKey.revoked()); issuedApiKey.setExpiresAt(createApiKey.expiresAt()); issuedApiKey.setLastUsedAt(createApiKey.lastUsedAt()); + issuedApiKey.setReferenceId(createApiKey.referenceId()); return issuedApiKey; } @@ -37,11 +40,12 @@ public static ResponseApiKey mapToResponse(IssuedApiKey issuedApiKey) { } return new ResponseApiKey( - issuedApiKey.getIssuedAt(), - issuedApiKey.getExpiresAt(), - issuedApiKey.getLastUsedAt(), + formatDateAndTime(issuedApiKey.getIssuedAt()), + formatDateAndTime(issuedApiKey.getExpiresAt()), + formatDateAndTime(issuedApiKey.getLastUsedAt()), issuedApiKey.getApiKeyName(), - issuedApiKey.isRevoked() + issuedApiKey.isRevoked(), + issuedApiKey.getReferenceId() ); @@ -64,4 +68,29 @@ public static GenerateApiKeyRequest normalizeExpiresAt(GenerateApiKeyRequest cre return new GenerateApiKeyRequest(createApiKey.apiKeyName(), inputDate); } + + public static String formatDateAndTime(LocalDateTime dateTime) { + if (dateTime != null) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + return dateTime.format(formatter); + } + return ""; + } + + public static IssuedApiKey updateIssuedApiKey(IssuedApiKey issuedApiKey, UpdateApiKey updateApiKey) { + if (issuedApiKey == null || issuedApiKey.getApiKeyName() == null) { + return null; + } + + if (updateApiKey.revoke() != null) { + issuedApiKey.setRevoked(updateApiKey.revoke()); + } + + if (updateApiKey.apiKeyName() != null) { + issuedApiKey.setApiKeyName(updateApiKey.apiKeyName()); + } + return issuedApiKey; + + } + } diff --git a/src/main/resources/db/migration/V7__adds_referenceID_uuid_to_apikey_table.sql b/src/main/resources/db/migration/V7__adds_referenceID_uuid_to_apikey_table.sql new file mode 100644 index 0000000..bae6d07 --- /dev/null +++ b/src/main/resources/db/migration/V7__adds_referenceID_uuid_to_apikey_table.sql @@ -0,0 +1,15 @@ + +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +ALTER TABLE issued_api_key + ADD COLUMN reference_id UUID; + +UPDATE issued_api_key +SET reference_id = gen_random_uuid() +WHERE reference_id IS NULL; + +ALTER TABLE issued_api_key + ALTER COLUMN reference_id SET NOT NULL; + +ALTER TABLE issued_api_key + ADD CONSTRAINT unique_reference_id UNIQUE (reference_id); diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index 2f9ffa3..cd07944 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -6,7 +6,10 @@ create.form.placeholder=Share your pawsome thoughts... create.post=Bark It! home=Home profile=Profile +mykeys=My Keys edit.profile=Edit Profile breed=Breed: footer=© 2025 Barkr - The Social Network for Dogs. All Rights Reserved. -success=Successfully barked! \ No newline at end of file +success=Successfully barked! +apikey.revoke=Revoke +apikey.nameupdate=Edit Name diff --git a/src/main/resources/messages_sv.properties b/src/main/resources/messages_sv.properties index 696fa42..c9eb7bb 100644 --- a/src/main/resources/messages_sv.properties +++ b/src/main/resources/messages_sv.properties @@ -6,7 +6,10 @@ create.form.placeholder=Dela med dig av dina tass-tastiska tankar... create.post=Voffa! home=Hem profile=Profil +mykeys=Mina Nycklar edit.profile=Redigera profil breed=Ras footer=© 2025 Barkr - Sociala nätverket för hundar. Alla rättigheter reserverade. -success=Lyckades voffa! \ No newline at end of file +success=Lyckades voffa! +apikey.revoke=Annulera +apikey.nameupdate=Byt Namn diff --git a/src/main/resources/static/css/styles.css b/src/main/resources/static/css/styles.css index d120920..04464ba 100644 --- a/src/main/resources/static/css/styles.css +++ b/src/main/resources/static/css/styles.css @@ -517,5 +517,10 @@ i { padding: 10px 20px; } + .table-responsive { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + } diff --git a/src/main/resources/static/js/update-apikey-name.js b/src/main/resources/static/js/update-apikey-name.js new file mode 100644 index 0000000..483fbb4 --- /dev/null +++ b/src/main/resources/static/js/update-apikey-name.js @@ -0,0 +1,10 @@ +function promptAndSetName(form) { + const input = form.querySelector('#nameUpdater'); + const currentValue = input.value; + + const newName = prompt("Enter a new name for the API key:", currentValue); + if (newName && newName.trim() !== "") { + input.value = newName.trim(); + form.submit(); + } +} diff --git a/src/main/resources/templates/apikeys/mykeys.html b/src/main/resources/templates/apikeys/mykeys.html index 7c77a97..79cd462 100644 --- a/src/main/resources/templates/apikeys/mykeys.html +++ b/src/main/resources/templates/apikeys/mykeys.html @@ -1,51 +1,109 @@ - + API Keys + + + + -

API Keys

- - - - - - - - - - - - - - - - - - - - - -
API Key NameIssued AtExpires AtLast Used AtRevokedActions
test2025-04-20T15:442025-04-20T15:492025-04-20T15:44false -
- - -
-
+ +
+
+
+ +
+

API Keys

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
API Key NameIssued AtExpires AtLast Used AtReference IDRevokedActions
test2025-04-20T15:442025-04-20T15:492025-04-20T15:4400-000-000-0000false +
+ + +
+ +
+ + + +
+
+
+
+ +
+ + + diff --git a/src/main/resources/templates/fragments/sidebar.html b/src/main/resources/templates/fragments/sidebar.html index e215774..395e0f2 100644 --- a/src/main/resources/templates/fragments/sidebar.html +++ b/src/main/resources/templates/fragments/sidebar.html @@ -7,6 +7,10 @@

Username Placeholder

Couldn't load dog fact

From b49385e191f105ab0694f83c0df364c345a75444 Mon Sep 17 00:00:00 2001 From: efpcode Date: Mon, 21 Apr 2025 02:41:58 +0200 Subject: [PATCH 24/51] Adds apikey validation for rest-api responses --- .../api/controller/ApiKeyController.java | 4 +- .../barkr/config/SecurityConfig.java | 13 ++++- .../barkr/domain/IssuedApiKeyService.java | 13 +++-- .../barkr/dto/apiKeyDto/UpdateApiKey.java | 4 +- .../filters/ApiKeyAuthenticationFilter.java | 50 ++++++++++++++++--- .../filters/ApiKeyAuthenticationToken.java | 2 +- .../storkforge/barkr/mapper/ApiKeyMapper.java | 4 ++ .../resources/templates/fragments/header.html | 4 +- 8 files changed, 74 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java b/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java index f97383a..fc0ab30 100644 --- a/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java +++ b/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java @@ -98,14 +98,14 @@ public String myKeys(Model model , @AuthenticationPrincipal OidcUser user) { @PostMapping("/mykeys/revoke") public String revokeKey(@RequestParam String referenceId) { - var update = new UpdateApiKey(UUID.fromString(referenceId),null, true); + var update = new UpdateApiKey(UUID.fromString(referenceId),null, true, null); issuedApiKeyService.updateApiKey(update); return "redirect:/apikeys/mykeys"; } @PostMapping("mykeys/nameupdate") public String nameUpdate(@RequestParam String apiKeyName, @RequestParam String referenceId) { - var update = new UpdateApiKey(UUID.fromString(referenceId), apiKeyName, false); + var update = new UpdateApiKey(UUID.fromString(referenceId), apiKeyName, false, null); issuedApiKeyService.updateApiKey(update); return "redirect:/apikeys/mykeys"; } diff --git a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java index 508f645..7359905 100644 --- a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java +++ b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java @@ -10,6 +10,7 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.storkforge.barkr.domain.IssuedApiKeyService; import org.storkforge.barkr.filters.ApiKeyAuthenticationFilter; @Configuration @@ -53,10 +54,12 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti @Bean @Order(2) - public SecurityFilterChain restAPIAndGraphQLFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain restAPIAndGraphQLFilterChain( + HttpSecurity http, + ApiKeyAuthenticationFilter apiKeyAuthenticationFilter) throws Exception { http.securityMatcher("/api/**", "/graphql") .csrf(AbstractHttpConfigurer::disable) - .addFilterAfter(new ApiKeyAuthenticationFilter(), LogoutFilter.class) + .addFilterAfter(apiKeyAuthenticationFilter, LogoutFilter.class) .authorizeHttpRequests( authorize -> authorize .requestMatchers("/api/accounts").authenticated() @@ -72,6 +75,12 @@ public SecurityFilterChain restAPIAndGraphQLFilterChain(HttpSecurity http) throw } + @Bean + @Order(1) + public ApiKeyAuthenticationFilter apiKeyAuthenticationFilter(IssuedApiKeyService issuedApiKeyService) { + return new ApiKeyAuthenticationFilter(issuedApiKeyService); + } + diff --git a/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java b/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java index 2f2c33c..0e1b05f 100644 --- a/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java +++ b/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java @@ -124,11 +124,10 @@ public boolean apiKeyExists(String hashedApiKey) { public boolean apiKeyValidation(String rawApiKey) throws NoSuchAlgorithmException, InvalidKeyException { var hashedApikey = hashedApiKey(rawApiKey); var keyFound = issuedApiKeyRepository.findByHashedApiKey(hashedApikey); - if (!keyFound.isPresent() || !keyFound.get().isRevoked()) { - return false; + if (keyFound.isPresent() && !keyFound.get().isRevoked()) { + return true; } - return true; - + return false; } @@ -137,7 +136,7 @@ public ResponseApiKeyList allApiKeys() { Authentication authentication = securityContext.getAuthentication(); var googleOidc2id = authentication.getName(); - var recordsRemoved = issuedApiKeyRepository.revokeExpiredKeys(LocalDateTime.now()); + var recordsRemoved = revokeExpiredApiKeys(); log.info("Removed " + recordsRemoved + " api keys"); var account = accountRepository.findByGoogleOidc2Id(googleOidc2id); @@ -157,6 +156,10 @@ public void updateApiKey(UpdateApiKey updateApiKey) { } + public int revokeExpiredApiKeys() { + return issuedApiKeyRepository.revokeExpiredKeys(LocalDateTime.now()); + } + diff --git a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/UpdateApiKey.java b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/UpdateApiKey.java index 092c8cd..d6fcbc2 100644 --- a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/UpdateApiKey.java +++ b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/UpdateApiKey.java @@ -3,10 +3,12 @@ import jakarta.validation.constraints.NotNull; import java.io.Serializable; +import java.time.LocalDateTime; import java.util.UUID; public record UpdateApiKey( @NotNull UUID referenceId, String apiKeyName, - Boolean revoke) implements Serializable { + Boolean revoke, + LocalDateTime lastUsedAt) implements Serializable { } diff --git a/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationFilter.java b/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationFilter.java index c731cdc..2d013a9 100644 --- a/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationFilter.java +++ b/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationFilter.java @@ -7,29 +7,65 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; +import org.storkforge.barkr.domain.IssuedApiKeyService; +import org.storkforge.barkr.dto.apiKeyDto.UpdateApiKey; import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.LocalDateTime; public class ApiKeyAuthenticationFilter extends OncePerRequestFilter { + IssuedApiKeyService issuedApiKeyService; + + public ApiKeyAuthenticationFilter(IssuedApiKeyService issuedApiKeyService) { + this.issuedApiKeyService = issuedApiKeyService; + } @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { String apiKey = request.getHeader("API-KEY"); - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + try { + if (isValidApiKey(apiKey)) { + Authentication auth = new ApiKeyAuthenticationToken(apiKey); + SecurityContextHolder.getContext().setAuthentication(auth); + + var hashedApiKey = issuedApiKeyService.hashedApiKey(apiKey); + var foundApikey = issuedApiKeyService.issuedApiKeyExists(hashedApiKey); + var updateApikey = new UpdateApiKey( + foundApikey.get().getReferenceId(), + null, + null, + LocalDateTime.now()); + issuedApiKeyService.updateApiKey(updateApikey); + + + } + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); - if (isValidApiKey(apiKey)) { - Authentication auth = new ApiKeyAuthenticationToken(apiKey); - SecurityContextHolder.getContext().setAuthentication(auth); + } catch (InvalidKeyException e) { + throw new RuntimeException(e); } + + filterChain.doFilter(request, response); } - private boolean isValidApiKey(String apiKey){ - return "mykey".equals(apiKey); + private boolean isValidApiKey(String apiKey) throws NoSuchAlgorithmException, InvalidKeyException { + issuedApiKeyService.revokeExpiredApiKeys(); + if (apiKey == null) { + return false; + } + return issuedApiKeyService.apiKeyValidation(apiKey); + } diff --git a/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationToken.java b/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationToken.java index 1b48dad..9ffa7a1 100644 --- a/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationToken.java +++ b/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationToken.java @@ -10,7 +10,7 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { private final String apiKey; public ApiKeyAuthenticationToken(String apiKey) { - super(Collections.singletonList(new SimpleGrantedAuthority("ROLE_APIKEY"))); + super(Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))); this.apiKey = apiKey; super.setAuthenticated(true); } diff --git a/src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java b/src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java index f63bab5..589b6ae 100644 --- a/src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java +++ b/src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java @@ -89,6 +89,10 @@ public static IssuedApiKey updateIssuedApiKey(IssuedApiKey issuedApiKey, UpdateA if (updateApiKey.apiKeyName() != null) { issuedApiKey.setApiKeyName(updateApiKey.apiKeyName()); } + + if(updateApiKey.lastUsedAt() != null) { + issuedApiKey.setLastUsedAt(updateApiKey.lastUsedAt()); + } return issuedApiKey; } diff --git a/src/main/resources/templates/fragments/header.html b/src/main/resources/templates/fragments/header.html index 152fdac..257d391 100644 --- a/src/main/resources/templates/fragments/header.html +++ b/src/main/resources/templates/fragments/header.html @@ -7,8 +7,8 @@

Barkr

- - + Sign in + Sign out
- - - + + + + diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index c90a374..939d349 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -30,8 +30,12 @@

Create a New Bark

-
- +
+ +
diff --git a/src/main/resources/templates/profile.html b/src/main/resources/templates/profile.html index 5b3670b..99be5bf 100644 --- a/src/main/resources/templates/profile.html +++ b/src/main/resources/templates/profile.html @@ -21,9 +21,21 @@
-
+ +
+

🎮 Unlock Premium

+
+
+ + +

+ +
+
+
@@ -79,6 +91,7 @@

Edit Profile

+ From 2eced2a6776819f1092b5ab53526b6d1a2b673ed Mon Sep 17 00:00:00 2001 From: efpcode Date: Mon, 21 Apr 2025 20:34:40 +0200 Subject: [PATCH 32/51] Adds translation --- src/main/resources/messages_en.properties | 2 ++ src/main/resources/messages_sv.properties | 2 ++ src/main/resources/templates/profile.html | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index cd07944..633efb1 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -13,3 +13,5 @@ footer=© 2025 Barkr - The Social Network for Dogs. All Rights Reserved. success=Successfully barked! apikey.revoke=Revoke apikey.nameupdate=Edit Name +code.label=Enter Secret Code: +code.button=Unlock diff --git a/src/main/resources/messages_sv.properties b/src/main/resources/messages_sv.properties index 5a91924..2d6bdf0 100644 --- a/src/main/resources/messages_sv.properties +++ b/src/main/resources/messages_sv.properties @@ -14,3 +14,5 @@ success=Lyckades voffa! apikey.revoke=Annulera apikey.nameupdate=Byt Namn apikey.gen= Skapa API Nyckel +code.label=Knappa in den hemliga koden +code.button=Lås Upp diff --git a/src/main/resources/templates/profile.html b/src/main/resources/templates/profile.html index 99be5bf..cdefd18 100644 --- a/src/main/resources/templates/profile.html +++ b/src/main/resources/templates/profile.html @@ -28,11 +28,11 @@

🎮 Unlock Premium

-
+


- +
From 79f874a738756c666cd480743311d138593c85cc Mon Sep 17 00:00:00 2001 From: efpcode Date: Mon, 21 Apr 2025 23:12:50 +0200 Subject: [PATCH 33/51] (fix) minor bug wiht profile loading --- .../barkr/api/controller/ApiKeyController.java | 2 +- .../org/storkforge/barkr/config/SecurityConfig.java | 11 ++++++++--- .../barkr/domain/CustomOidc2UserService.java | 3 +-- .../barkr/web/controller/WebController.java | 1 + 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java b/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java index fc0ab30..56e8cc4 100644 --- a/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java +++ b/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java @@ -57,7 +57,7 @@ public String showGeneratedApiKey(@ModelAttribute("response") ResponseApiKeyOnce @PostMapping("/generate") - @PreAuthorize("hasRole('ROLE_USER')") + @PreAuthorize("hasRole('USER')") public String generateApiKey( @RequestParam String apiKeyName, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME diff --git a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java index 6c2f591..ed0ab2a 100644 --- a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java +++ b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java @@ -11,6 +11,7 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.logout.LogoutFilter; import org.storkforge.barkr.domain.IssuedApiKeyService; +import org.storkforge.barkr.domain.roles.BarkrRole; import org.storkforge.barkr.filters.ApiKeyAuthenticationFilter; @Configuration @@ -27,7 +28,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/post/load").permitAll() .requestMatchers("/barkr/logout").permitAll() .requestMatchers( "/account/{id}/image").permitAll() - .requestMatchers("/ai/generate").authenticated() + .requestMatchers("/ai/generate").hasRole(BarkrRole.PREMIUM.name()) .requestMatchers("/post/add").authenticated() .requestMatchers("/account/{id}/upload").authenticated() .requestMatchers("/{username}").authenticated() @@ -43,8 +44,12 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti ) .logout(logout -> logout - .logoutUrl("/logout") - .logoutSuccessUrl("/barkr/logout") + .logoutUrl("/logout") + .invalidateHttpSession(true) + .clearAuthentication(true) + .deleteCookies("JSESSIONID") + .logoutSuccessUrl("/barkr/logout") + .permitAll() ); diff --git a/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java b/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java index 63f46af..e4b995f 100644 --- a/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java +++ b/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java @@ -4,7 +4,6 @@ import org.slf4j.LoggerFactory; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; @@ -40,7 +39,7 @@ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2Authenticatio String oidcId = user.getName(); Account account; - var accountFound = accountRepository.findByUsernameEqualsIgnoreCase(username); + var accountFound = accountRepository.findByGoogleOidc2Id(user.getName()); if (accountFound.isPresent()) { account = accountFound.get(); diff --git a/src/main/java/org/storkforge/barkr/web/controller/WebController.java b/src/main/java/org/storkforge/barkr/web/controller/WebController.java index 01f6d99..6f4fc54 100644 --- a/src/main/java/org/storkforge/barkr/web/controller/WebController.java +++ b/src/main/java/org/storkforge/barkr/web/controller/WebController.java @@ -78,6 +78,7 @@ public String user(@PathVariable("username") @NotBlank String username, Model mo if(user != null) { var currentUser = accountService.findByGoogleOidc2Id(user.getName()); id = currentUser.isEmpty() ? 1L : currentUser.get().getId(); + username = currentUser.get().getUsername().isEmpty() ? username : currentUser.get().getUsername(); } ResponseAccount queryAccount; try { From 573ca4e64db214bf2ec3277037925fb309ac8727 Mon Sep 17 00:00:00 2001 From: efpcode Date: Mon, 21 Apr 2025 23:16:46 +0200 Subject: [PATCH 34/51] minor fix --- .../java/org/storkforge/barkr/web/controller/WebController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/storkforge/barkr/web/controller/WebController.java b/src/main/java/org/storkforge/barkr/web/controller/WebController.java index 6f4fc54..71a15b8 100644 --- a/src/main/java/org/storkforge/barkr/web/controller/WebController.java +++ b/src/main/java/org/storkforge/barkr/web/controller/WebController.java @@ -174,7 +174,7 @@ public String uploadImage(@PathVariable @Positive @NotNull Long id, @RequestPara } - @PostMapping("/unlock-easter-egg") +@PostMapping("/unlock-easter-egg") public ResponseEntity premiumUser(@RequestParam String code, @AuthenticationPrincipal OidcUser user){ var sanitizedCode = code.toLowerCase().replaceAll("[^a-zA-Z0-9]", ""); From 6ef9dcfcf08e478a94ef0a5e80085c6920b6daed Mon Sep 17 00:00:00 2001 From: efpcode Date: Mon, 21 Apr 2025 23:31:11 +0200 Subject: [PATCH 35/51] removes spaces --- .../java/org/storkforge/barkr/web/controller/WebController.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/org/storkforge/barkr/web/controller/WebController.java b/src/main/java/org/storkforge/barkr/web/controller/WebController.java index 71a15b8..6b99f8a 100644 --- a/src/main/java/org/storkforge/barkr/web/controller/WebController.java +++ b/src/main/java/org/storkforge/barkr/web/controller/WebController.java @@ -200,8 +200,6 @@ public ResponseEntity premiumUser(@RequestParam String code, @AuthenticationP return ResponseEntity.status(HttpStatus.ACCEPTED).body("Congrats: \uD83D\uDC4D"); - - } From b6ffe92a290ff07d58a21682b92b1ed75194d867 Mon Sep 17 00:00:00 2001 From: efpcode Date: Tue, 22 Apr 2025 00:43:31 +0200 Subject: [PATCH 36/51] (fix) WebConctrollerTest passisng after Security changes --- src/main/resources/static/js/generate-joke.js | 22 +++++--- .../resources/static/js/unlock-easter-egg.js | 54 ++++++++++--------- .../web/controller/WebControllerTest.java | 33 ++++++++++++ 3 files changed, 77 insertions(+), 32 deletions(-) diff --git a/src/main/resources/static/js/generate-joke.js b/src/main/resources/static/js/generate-joke.js index fa1e4e1..3c91c13 100644 --- a/src/main/resources/static/js/generate-joke.js +++ b/src/main/resources/static/js/generate-joke.js @@ -1,8 +1,16 @@ -document.getElementById('generate-joke-btn').addEventListener('click', function() { - fetch('/ai/generate') - .then(response => response.json()) - .then(data => { - document.getElementById('joke-display').textContent = data.generation; - }) - .catch(error => console.error('Error fetching joke:', error)); + +document.addEventListener("DOMContentLoaded", function() { + const jokeBtn = document.getElementById('generate-joke-btn'); + if (jokeBtn) { + jokeBtn.addEventListener('click', function() { + fetch('/ai/generate') + .then(response => response.json()) + .then(data => { + document.getElementById('joke-display').textContent = data.generation; + }) + .catch(error => console.error('Error fetching joke:', error)); + }); + } else { + console.error("Button #generate-joke-btn not found! User might not have ROLE_PREMIUM."); + } }); diff --git a/src/main/resources/static/js/unlock-easter-egg.js b/src/main/resources/static/js/unlock-easter-egg.js index 5667446..7d6fae9 100644 --- a/src/main/resources/static/js/unlock-easter-egg.js +++ b/src/main/resources/static/js/unlock-easter-egg.js @@ -1,28 +1,32 @@ - document.getElementById('unlock-premium-form').addEventListener('submit', function(event) { - event.preventDefault(); // 💥 CRUCIAL: Prevent the form from reloading the page +document.addEventListener('DOMContentLoaded', function () { + var form = document.getElementById('unlock-premium-form'); + if (!form) return; - const code = document.getElementById('code').value; - const csrfToken = document.getElementById('csrfToken').value; + form.addEventListener('submit', function(event) { + event.preventDefault(); - fetch('/unlock-easter-egg', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRF-TOKEN': csrfToken -}, - body: new URLSearchParams({ code }) -}) - .then(async response => { - const message = await response.text(); - if (!response.ok) { - throw new Error(message); -} - return message; -}) - .then(data => { - document.getElementById('premium-response').textContent = data; -}) - .catch(error => { - document.getElementById('premium-response').textContent = '❌ ' + error.message; -}); + var code = document.getElementById('code').value; + var csrfToken = document.getElementById('csrfToken').value; + + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/unlock-easter-egg', true); + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + xhr.setRequestHeader('X-CSRF-TOKEN', csrfToken); + + var body = 'code=' + encodeURIComponent(code); // <-- manually build form data string + + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + var responseText = xhr.responseText; + if (xhr.status === 200 || xhr.status === 202) { + document.getElementById('premium-response').textContent = responseText; + } else { + console.error("XHR error:", responseText); + document.getElementById('premium-response').textContent = '❌ ' + responseText; + } + } + }; + + xhr.send(body); + }); }); diff --git a/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java b/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java index e1c57dc..e93cf26 100644 --- a/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java +++ b/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java @@ -11,13 +11,16 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.data.annotation.Transient; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import org.storkforge.barkr.domain.entity.Account; +import org.storkforge.barkr.domain.entity.GoogleAccountApiKeyLink; import org.storkforge.barkr.domain.entity.Post; +import org.storkforge.barkr.domain.roles.BarkrRole; import org.storkforge.barkr.infrastructure.persistence.AccountRepository; import org.storkforge.barkr.infrastructure.persistence.PostRepository; import org.testcontainers.containers.PostgreSQLContainer; @@ -26,8 +29,10 @@ import org.testcontainers.utility.DockerImageName; import java.io.IOException; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -50,6 +55,9 @@ class WebControllerTest { @Autowired private AccountRepository accountRepository; + @Transient + private GoogleAccountApiKeyLink googleAccountApiKeyLink; + @Autowired private PostRepository postRepository; @@ -72,12 +80,24 @@ class IndexRouteTest { @DisplayName("Can view all posts") void viewAllPostsPage() throws IOException { Account mockAccount = new Account(); + GoogleAccountApiKeyLink mockKeyLink = new GoogleAccountApiKeyLink(); mockAccount.setUsername("mockAccount"); mockAccount.setBreed("husky"); + mockAccount.setGoogleOidc2Id("4"); + mockAccount.setImage(null); + mockAccount.setRoles(new HashSet<>(Set.of(BarkrRole.USER, BarkrRole.PREMIUM))); + mockKeyLink.setAccount(mockAccount); + mockAccount.setGoogleAccountApiKeyLink(mockKeyLink); Account mockAccount2 = new Account(); + GoogleAccountApiKeyLink mockKeyLink2 = new GoogleAccountApiKeyLink(); mockAccount2.setUsername("mockAccount2"); mockAccount2.setBreed("beagle"); + mockAccount2.setGoogleOidc2Id("5"); + mockAccount2.setImage(null); + mockAccount2.setRoles(new HashSet<>(Set.of(BarkrRole.USER, BarkrRole.PREMIUM))); + mockKeyLink2.setAccount(mockAccount2); + mockAccount2.setGoogleAccountApiKeyLink(mockKeyLink2); accountRepository.saveAll(List.of(mockAccount, mockAccount2)); @@ -177,12 +197,25 @@ class ProfileRouteTest { @DisplayName("Can view profile page") void viewProfilePage() throws IOException { Account mockAccount = new Account(); + GoogleAccountApiKeyLink mockKeyLink = new GoogleAccountApiKeyLink(); mockAccount.setUsername("mockAccount"); mockAccount.setBreed("husky"); + mockAccount.setGoogleOidc2Id("4"); + mockAccount.setImage(null); + mockAccount.setRoles(new HashSet<>(Set.of(BarkrRole.USER, BarkrRole.PREMIUM))); + mockKeyLink.setAccount(mockAccount); + mockAccount.setGoogleAccountApiKeyLink(mockKeyLink); Account mockAccount2 = new Account(); + GoogleAccountApiKeyLink mockKeyLink2 = new GoogleAccountApiKeyLink(); mockAccount2.setUsername("mockAccount2"); mockAccount2.setBreed("beagle"); + mockAccount2.setBreed("beagle"); + mockAccount2.setGoogleOidc2Id("5"); + mockAccount2.setImage(null); + mockAccount2.setRoles(new HashSet<>(Set.of(BarkrRole.USER, BarkrRole.PREMIUM))); + mockKeyLink2.setAccount(mockAccount2); + mockAccount2.setGoogleAccountApiKeyLink(mockKeyLink2); accountRepository.saveAll(List.of(mockAccount, mockAccount2)); From 28fc5a942c2a08dd66db370b3a7ca1fd8a853964 Mon Sep 17 00:00:00 2001 From: efpcode Date: Tue, 22 Apr 2025 04:45:34 +0200 Subject: [PATCH 37/51] (fix) after roast from ai --- .../ai/controller/MistralAiController.java | 1 - .../api/controller/ApiAccountController.java | 1 - .../api/controller/ApiKeyController.java | 2 +- .../barkr/config/SecurityConfig.java | 2 ++ .../barkr/domain/CustomOidc2UserService.java | 33 +++++++++++-------- .../barkr/domain/IssuedApiKeyService.java | 2 +- .../storkforge/barkr/domain/PostService.java | 1 - .../barkr/domain/entity/IssuedApiKey.java | 2 +- .../barkr/dto/apiKeyDto/CreateApiKey.java | 2 +- .../barkr/dto/apiKeyDto/ResponseApiKey.java | 2 -- .../filters/ApiKeyAuthenticationFilter.java | 15 +++++---- .../storkforge/barkr/mapper/ApiKeyMapper.java | 8 +++-- .../barkr/web/controller/WebController.java | 10 +++++- src/main/resources/messages.properties | 3 +- .../resources/templates/fragments/header.html | 7 ++-- .../templates/fragments/sidebar.html | 2 +- 16 files changed, 57 insertions(+), 36 deletions(-) diff --git a/src/main/java/org/storkforge/barkr/ai/controller/MistralAiController.java b/src/main/java/org/storkforge/barkr/ai/controller/MistralAiController.java index fdc0a58..38015a7 100644 --- a/src/main/java/org/storkforge/barkr/ai/controller/MistralAiController.java +++ b/src/main/java/org/storkforge/barkr/ai/controller/MistralAiController.java @@ -8,7 +8,6 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -import org.storkforge.barkr.domain.roles.BarkrRole; import java.util.Map; diff --git a/src/main/java/org/storkforge/barkr/api/controller/ApiAccountController.java b/src/main/java/org/storkforge/barkr/api/controller/ApiAccountController.java index 8a9395a..d7fc91e 100644 --- a/src/main/java/org/storkforge/barkr/api/controller/ApiAccountController.java +++ b/src/main/java/org/storkforge/barkr/api/controller/ApiAccountController.java @@ -3,7 +3,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; -import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java b/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java index 56e8cc4..7fdd88e 100644 --- a/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java +++ b/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java @@ -73,8 +73,8 @@ public String generateApiKey( do{ apiKey = issuedApiKeyService.generateRawApiKey(); hashedApiKey = issuedApiKeyService.hashedApiKey(apiKey); - issuedApiKeyService.apiKeyGenerate(request, hashedApiKey); } while (issuedApiKeyService.apiKeyExists(hashedApiKey)); + issuedApiKeyService.apiKeyGenerate(request, hashedApiKey); ResponseApiKeyOnce response = new ResponseApiKeyOnce( "API-KEY", apiKey, "Please store these credentials safely"); diff --git a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java index ed0ab2a..f063bb3 100644 --- a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java +++ b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java @@ -8,6 +8,7 @@ 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.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.logout.LogoutFilter; import org.storkforge.barkr.domain.IssuedApiKeyService; @@ -64,6 +65,7 @@ public SecurityFilterChain restAPIAndGraphQLFilterChain( HttpSecurity http, ApiKeyAuthenticationFilter apiKeyAuthenticationFilter) throws Exception { http.securityMatcher("/api/**", "/graphql") + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .csrf(AbstractHttpConfigurer::disable) .addFilterAfter(apiKeyAuthenticationFilter, LogoutFilter.class) .authorizeHttpRequests( diff --git a/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java b/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java index e4b995f..25bafce 100644 --- a/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java +++ b/src/main/java/org/storkforge/barkr/domain/CustomOidc2UserService.java @@ -33,7 +33,7 @@ public CustomOidc2UserService(AccountRepository accountRepository) { @Override - public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException{ + public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { OidcUser user = super.loadUser(userRequest); String username = user.getAttributes().get("name").toString(); String oidcId = user.getName(); @@ -44,19 +44,24 @@ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2Authenticatio if (accountFound.isPresent()) { account = accountFound.get(); - }else{ - - account = new Account(); - GoogleAccountApiKeyLink link = new GoogleAccountApiKeyLink(); - account.setUsername(username); - account.setBreed("Snoopy"); - account.setGoogleOidc2Id(oidcId); - account.setImage(null); - account.setRoles(new HashSet<>(Set.of(BarkrRole.USER))); - link.setAccount(account); - log.info("New record added to database"); - account.setGoogleAccountApiKeyLink(link); - accountRepository.save(account); + } else { + + account = new Account(); + GoogleAccountApiKeyLink link = new GoogleAccountApiKeyLink(); + account.setUsername(username); + account.setBreed("Snoopy"); + account.setGoogleOidc2Id(oidcId); + account.setImage(null); + account.setRoles(new HashSet<>(Set.of(BarkrRole.USER))); + link.setAccount(account); + log.info("New record added to database"); + account.setGoogleAccountApiKeyLink(link); + try { + + accountRepository.save(account); + } catch (Exception e) { + throw new OAuth2AuthenticationException("Failed to save account " + account); + } } Set mappedAuthorities = account.getRoles().stream() diff --git a/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java b/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java index 0e1b05f..4c6d317 100644 --- a/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java +++ b/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java @@ -118,7 +118,7 @@ public String hashedApiKey(String rawApiKey) throws NoSuchAlgorithmException, In public boolean apiKeyExists(String hashedApiKey) { log.info("Checking if issued api key exists"); - return issuedApiKeyRepository.findByHashedApiKey(hashedApiKey).isEmpty(); + return issuedApiKeyRepository.findByHashedApiKey(hashedApiKey).isPresent(); } public boolean apiKeyValidation(String rawApiKey) throws NoSuchAlgorithmException, InvalidKeyException { diff --git a/src/main/java/org/storkforge/barkr/domain/PostService.java b/src/main/java/org/storkforge/barkr/domain/PostService.java index 4a52503..6a49e74 100644 --- a/src/main/java/org/storkforge/barkr/domain/PostService.java +++ b/src/main/java/org/storkforge/barkr/domain/PostService.java @@ -18,7 +18,6 @@ import org.storkforge.barkr.infrastructure.persistence.PostRepository; import org.storkforge.barkr.mapper.PostMapper; -import java.util.List; @Service @Transactional diff --git a/src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java b/src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java index 50ef358..7f1586e 100644 --- a/src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java +++ b/src/main/java/org/storkforge/barkr/domain/entity/IssuedApiKey.java @@ -159,7 +159,7 @@ public final int hashCode() { public String toString() { return getClass().getSimpleName() + "(" + "id = " + id + ", " + - "hashedApiKey = " + hashedApiKey + ", " + + "hashedApiKey = [REDACTED], " + "issuedAt = " + issuedAt + ", " + "expiresAt = " + expiresAt + ", " + "revoked = " + revoked + ", " + diff --git a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/CreateApiKey.java b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/CreateApiKey.java index 5156028..97be937 100644 --- a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/CreateApiKey.java +++ b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/CreateApiKey.java @@ -18,5 +18,5 @@ public record CreateApiKey( @PastOrPresent LocalDateTime lastUsedAt, @NotBlank String apiKeyName, @NotNull GoogleAccountApiKeyLink googleAccountApiKeyLink, - @NotBlank UUID referenceId) implements Serializable { + @NotNull UUID referenceId) implements Serializable { } diff --git a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKey.java b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKey.java index 96c349f..9804288 100644 --- a/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKey.java +++ b/src/main/java/org/storkforge/barkr/dto/apiKeyDto/ResponseApiKey.java @@ -1,9 +1,7 @@ package org.storkforge.barkr.dto.apiKeyDto; -import jakarta.validation.constraints.FutureOrPresent; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.PastOrPresent; import java.io.Serializable; import java.util.UUID; diff --git a/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationFilter.java b/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationFilter.java index 2d013a9..8c66b14 100644 --- a/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationFilter.java +++ b/src/main/java/org/storkforge/barkr/filters/ApiKeyAuthenticationFilter.java @@ -38,12 +38,15 @@ protected void doFilterInternal( var hashedApiKey = issuedApiKeyService.hashedApiKey(apiKey); var foundApikey = issuedApiKeyService.issuedApiKeyExists(hashedApiKey); - var updateApikey = new UpdateApiKey( - foundApikey.get().getReferenceId(), - null, - null, - LocalDateTime.now()); - issuedApiKeyService.updateApiKey(updateApikey); + + if (foundApikey.isPresent()) { + var updateApikey = new UpdateApiKey( + foundApikey.get().getReferenceId(), + null, + null, + LocalDateTime.now()); + issuedApiKeyService.updateApiKey(updateApikey); + } } diff --git a/src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java b/src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java index 589b6ae..c03fe9d 100644 --- a/src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java +++ b/src/main/java/org/storkforge/barkr/mapper/ApiKeyMapper.java @@ -55,6 +55,10 @@ public static GenerateApiKeyRequest normalizeExpiresAt(GenerateApiKeyRequest cre LocalDateTime now = LocalDateTime.now(); LocalDateTime inputDate = createApiKey.expiresAt(); + if (inputDate == null) { + inputDate = LocalDateTime.now(); + } + if (inputDate != null && inputDate.isBefore(now)) { inputDate = LocalDateTime.now().plusMinutes(5); } @@ -78,7 +82,7 @@ public static String formatDateAndTime(LocalDateTime dateTime) { } public static IssuedApiKey updateIssuedApiKey(IssuedApiKey issuedApiKey, UpdateApiKey updateApiKey) { - if (issuedApiKey == null || issuedApiKey.getApiKeyName() == null) { + if (issuedApiKey == null || updateApiKey == null || issuedApiKey.getApiKeyName() == null) { return null; } @@ -91,7 +95,7 @@ public static IssuedApiKey updateIssuedApiKey(IssuedApiKey issuedApiKey, UpdateA } if(updateApiKey.lastUsedAt() != null) { - issuedApiKey.setLastUsedAt(updateApiKey.lastUsedAt()); + issuedApiKey.setLastUsedAt(LocalDateTime.now()); } return issuedApiKey; diff --git a/src/main/java/org/storkforge/barkr/web/controller/WebController.java b/src/main/java/org/storkforge/barkr/web/controller/WebController.java index 6b99f8a..95cb7bd 100644 --- a/src/main/java/org/storkforge/barkr/web/controller/WebController.java +++ b/src/main/java/org/storkforge/barkr/web/controller/WebController.java @@ -177,6 +177,10 @@ public String uploadImage(@PathVariable @Positive @NotNull Long id, @RequestPara @PostMapping("/unlock-easter-egg") public ResponseEntity premiumUser(@RequestParam String code, @AuthenticationPrincipal OidcUser user){ + if(user == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Login Required \uD83D\uDE1C"); + } + var sanitizedCode = code.toLowerCase().replaceAll("[^a-zA-Z0-9]", ""); if (!sanitizedCode.trim().equals("upupdowndownleftrightleftrightba")) { @@ -192,7 +196,11 @@ public ResponseEntity premiumUser(@RequestParam String code, @AuthenticationP .collect(Collectors.toSet()); OidcUser updateUser = new DefaultOidcUser(mappedAuthorities, user.getIdToken(), user.getUserInfo()); - OAuth2AuthenticationToken auth = new OAuth2AuthenticationToken(updateUser, updateUser.getAuthorities(), updateUser.getAccessTokenHash()); + String registrationId = ((OAuth2AuthenticationToken) SecurityContextHolder.getContext() + .getAuthentication()) + .getAuthorizedClientRegistrationId(); + + OAuth2AuthenticationToken auth = new OAuth2AuthenticationToken(updateUser, updateUser.getAuthorities(), registrationId); SecurityContext context = SecurityContextHolder.getContext(); context.setAuthentication(auth); diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index ebeaccc..865d68c 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -7,7 +7,8 @@ create.post=Bark It! home=Home profile=Profile edit.profile=Edit Profile -apikey.gen = Generate API Key +apikey.gen=Generate API Key breed=Breed: footer=� 2025 Barkr - The Social Network for Dogs. All Rights Reserved. success=Successfully barked! +mykeys=My Keys diff --git a/src/main/resources/templates/fragments/header.html b/src/main/resources/templates/fragments/header.html index 257d391..743a7b1 100644 --- a/src/main/resources/templates/fragments/header.html +++ b/src/main/resources/templates/fragments/header.html @@ -7,8 +7,11 @@

Barkr

- Sign in - Sign out + Sign in +
+ + +
- + +
+ +
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 939d349..cb2c02f 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -10,16 +10,15 @@
-
+

Create a New Bark

-
+
- +
@@ -31,7 +30,6 @@

Create a New Bark

-
diff --git a/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java b/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java index e93cf26..4eb5bbc 100644 --- a/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java +++ b/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java @@ -17,6 +17,7 @@ import org.springframework.cache.CacheManager; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; +import org.storkforge.barkr.domain.IssuedApiKeyService; import org.storkforge.barkr.domain.entity.Account; import org.storkforge.barkr.domain.entity.GoogleAccountApiKeyLink; import org.storkforge.barkr.domain.entity.Post; @@ -58,6 +59,9 @@ class WebControllerTest { @Transient private GoogleAccountApiKeyLink googleAccountApiKeyLink; + @Transient + private IssuedApiKeyService issuedApiKeyService; + @Autowired private PostRepository postRepository; @@ -74,27 +78,36 @@ void clearCache() { ); } + + @Nested class IndexRouteTest { @Test + @WithMockUser @DisplayName("Can view all posts") void viewAllPostsPage() throws IOException { Account mockAccount = new Account(); GoogleAccountApiKeyLink mockKeyLink = new GoogleAccountApiKeyLink(); + mockAccount.setUsername("mockAccount"); mockAccount.setBreed("husky"); mockAccount.setGoogleOidc2Id("4"); + mockAccount.setImage(null); mockAccount.setRoles(new HashSet<>(Set.of(BarkrRole.USER, BarkrRole.PREMIUM))); + mockKeyLink.setAccount(mockAccount); mockAccount.setGoogleAccountApiKeyLink(mockKeyLink); Account mockAccount2 = new Account(); GoogleAccountApiKeyLink mockKeyLink2 = new GoogleAccountApiKeyLink(); + mockAccount2.setUsername("mockAccount2"); mockAccount2.setBreed("beagle"); + mockAccount2.setGoogleOidc2Id("5"); mockAccount2.setImage(null); + mockAccount2.setRoles(new HashSet<>(Set.of(BarkrRole.USER, BarkrRole.PREMIUM))); mockKeyLink2.setAccount(mockAccount2); mockAccount2.setGoogleAccountApiKeyLink(mockKeyLink2); @@ -121,10 +134,11 @@ void viewAllPostsPage() throws IOException { } @Test + @WithMockUser(authorities = {"ROLE_USER", "ROLE_PREMIUM"}) @DisplayName("Verify add post form exist") void verifyAddPostFormExist() throws IOException { HtmlPage page = htmlClient.getPage("/"); - HtmlForm form = page.getForms().getFirst(); + HtmlForm form = page.getFormByName("index-first-form"); HtmlTextArea contentInput = form.getTextAreaByName("content"); assertAll( @@ -142,7 +156,7 @@ void verifyAddPostFormExist() throws IOException { void verifyAddPostFormSubmitted() throws IOException { HtmlPage page = htmlClient.getPage("/"); - HtmlForm form = page.getForms().getFirst(); + HtmlForm form = page.getFormByName("index-first-form"); HtmlTextArea contentInput = form.getTextAreaByName("content"); HtmlButton submitButton = (HtmlButton) form.getElementsByTagName("button").getFirst(); @@ -163,7 +177,8 @@ void verifyAddPostFormSubmitted() throws IOException { void redirectOnEmptyForm() throws IOException { HtmlPage page = htmlClient.getPage("/"); - HtmlForm form = page.getForms().getFirst(); + + HtmlForm form = page.getFormByName("index-first-form"); HtmlButton submitButton = (HtmlButton) form.getElementsByTagName("button").getFirst(); HtmlPage resultPage = submitButton.click(); @@ -177,7 +192,7 @@ void redirectOnEmptyForm() throws IOException { void redirectOnTooManyCharactersInput() throws IOException { HtmlPage page = htmlClient.getPage("/"); - HtmlForm form = page.getForms().getFirst(); + HtmlForm form = page.getFormByName("index-first-form"); HtmlTextArea contentInput = form.getTextAreaByName("content"); HtmlButton submitButton = (HtmlButton) form.getElementsByTagName("button").getFirst(); @@ -210,7 +225,6 @@ void viewProfilePage() throws IOException { GoogleAccountApiKeyLink mockKeyLink2 = new GoogleAccountApiKeyLink(); mockAccount2.setUsername("mockAccount2"); mockAccount2.setBreed("beagle"); - mockAccount2.setBreed("beagle"); mockAccount2.setGoogleOidc2Id("5"); mockAccount2.setImage(null); mockAccount2.setRoles(new HashSet<>(Set.of(BarkrRole.USER, BarkrRole.PREMIUM))); @@ -229,6 +243,8 @@ void viewProfilePage() throws IOException { postRepository.saveAll(List.of(mockPost, mockPost2)); HtmlPage page = htmlClient.getPage("/mockAccount"); + + mockAccount2.setRoles(Set.of()); String pageContent = page.asNormalizedText(); assertAll( From 20f9454af02ebf8e8de16a47026c5c391a113d84 Mon Sep 17 00:00:00 2001 From: efpcode Date: Wed, 23 Apr 2025 08:43:44 +0200 Subject: [PATCH 40/51] !(fix) adds essential envs var to application profile --- .../java/org/storkforge/barkr/config/SecurityConfig.java | 1 - src/main/resources/application-dev.properties | 2 +- src/main/resources/application.properties | 6 ++++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java index b5dc3bd..8c5a5c5 100644 --- a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java +++ b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java @@ -12,7 +12,6 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.logout.LogoutFilter; -import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.storkforge.barkr.domain.IssuedApiKeyService; import org.storkforge.barkr.domain.roles.BarkrRole; import org.storkforge.barkr.filters.ApiKeyAuthenticationFilter; diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index fcdf3e1..703e238 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -20,4 +20,4 @@ spring.security.oauth2.client.registration.google.redirect-uri={baseUrl}/login/o # Custom security key -barkr.api.secret = ${SECRET_KEY} +barkr.api.secret=${SECRET_KEY} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index da538f0..ec57cee 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,3 +3,9 @@ spring.messages.fallback-to-system-locale=false server.error.include-stacktrace=never spring.servlet.multipart.max-file-size=5MB ai.mistral.joke-prompt=tell me a dog joke like you are a dog and dont write anything else and dont explain the joke +spring.ai.mistralai.api-key=${MISTRAL_API_KEY} +barkr.api.secret=${SECRET_KEY:default-secret-key} +spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID} +spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET} +spring.security.oauth2.client.registration.google.scope=openid, profile, email +spring.security.oauth2.client.registration.google.redirect-uri={baseUrl}/login/oauth2/code/google From 3e8b927956b09c657713a096e386f3087ebf7a8c Mon Sep 17 00:00:00 2001 From: efpcode Date: Wed, 23 Apr 2025 08:58:25 +0200 Subject: [PATCH 41/51] (fix) Secret in constructor --- .../storkforge/barkr/domain/IssuedApiKeyService.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java b/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java index 4c6d317..0733aee 100644 --- a/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java +++ b/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java @@ -38,19 +38,17 @@ public class IssuedApiKeyService { private final IssuedApiKeyRepository issuedApiKeyRepository; private final GoogleAccountApiKeyLinkRepository googleAccountApiKeyLinkRepository; private final AccountRepository accountRepository; - private final String secretKey; + @Value("${barkr.api.secret}") + private String secretKey; public IssuedApiKeyService( IssuedApiKeyRepository issuedApiKeyRepository, GoogleAccountApiKeyLinkRepository googleAccountApiKeyLinkRepository, - AccountRepository accountRepository, - @Value("${barkr.api.secret}") - String secretKey) { + AccountRepository accountRepository) { this.issuedApiKeyRepository = issuedApiKeyRepository; this.googleAccountApiKeyLinkRepository = googleAccountApiKeyLinkRepository; this.accountRepository = accountRepository; - this.secretKey = secretKey; } @@ -107,7 +105,7 @@ public UUID generateUuid() { public String hashedApiKey(String rawApiKey) throws NoSuchAlgorithmException, InvalidKeyException { - SecretKeySpec keySpec = new SecretKeySpec(this.secretKey.getBytes(), "HmacSHA256"); + SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(keySpec); From a2661ba454b18d143ab7f930dcf6822ee7161c5e Mon Sep 17 00:00:00 2001 From: efpcode Date: Wed, 23 Apr 2025 09:15:19 +0200 Subject: [PATCH 42/51] Adds new dependencies to failing test --- .../storkforge/barkr/web/controller/WebControllerTest.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java b/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java index 4eb5bbc..05c0b3b 100644 --- a/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java +++ b/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java @@ -22,6 +22,7 @@ import org.storkforge.barkr.domain.entity.GoogleAccountApiKeyLink; import org.storkforge.barkr.domain.entity.Post; import org.storkforge.barkr.domain.roles.BarkrRole; +import org.storkforge.barkr.filters.ApiKeyAuthenticationFilter; import org.storkforge.barkr.infrastructure.persistence.AccountRepository; import org.storkforge.barkr.infrastructure.persistence.PostRepository; import org.testcontainers.containers.PostgreSQLContainer; @@ -59,9 +60,13 @@ class WebControllerTest { @Transient private GoogleAccountApiKeyLink googleAccountApiKeyLink; - @Transient + @Autowired private IssuedApiKeyService issuedApiKeyService; + @Autowired + private ApiKeyAuthenticationFilter apiKeyAuthenticationFilter; + + @Autowired private PostRepository postRepository; From e081e188170c8f2e68b9d39d6e2c471f1d434b71 Mon Sep 17 00:00:00 2001 From: efpcode Date: Wed, 23 Apr 2025 10:17:05 +0200 Subject: [PATCH 43/51] (fix) removes bean ApiKeyAuthenticationFilter from SecurityConfiguration --- .../org/storkforge/barkr/config/SecurityConfig.java | 10 +++------- src/main/resources/application.properties | 1 + .../barkr/web/controller/WebControllerTest.java | 13 ++----------- 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java index 8c5a5c5..a396f20 100644 --- a/src/main/java/org/storkforge/barkr/config/SecurityConfig.java +++ b/src/main/java/org/storkforge/barkr/config/SecurityConfig.java @@ -65,11 +65,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti @Order(2) public SecurityFilterChain restAPIAndGraphQLFilterChain( HttpSecurity http, - ApiKeyAuthenticationFilter apiKeyAuthenticationFilter) throws Exception { + IssuedApiKeyService issuedApiKeyService) throws Exception { http.securityMatcher("/api/**", "/graphql") .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .csrf(AbstractHttpConfigurer::disable) - .addFilterAfter(apiKeyAuthenticationFilter, LogoutFilter.class) + .addFilterAfter(new ApiKeyAuthenticationFilter(issuedApiKeyService), LogoutFilter.class) .authorizeHttpRequests( authorize -> authorize .requestMatchers("/api/accounts").authenticated() @@ -85,11 +85,7 @@ public SecurityFilterChain restAPIAndGraphQLFilterChain( } - @Bean - @Order(1) - public ApiKeyAuthenticationFilter apiKeyAuthenticationFilter(IssuedApiKeyService issuedApiKeyService) { - return new ApiKeyAuthenticationFilter(issuedApiKeyService); - } + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ec57cee..fa90998 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,6 +2,7 @@ spring.application.name=barkr spring.messages.fallback-to-system-locale=false server.error.include-stacktrace=never spring.servlet.multipart.max-file-size=5MB +spring.config.import=optional:file:.env[.properties] ai.mistral.joke-prompt=tell me a dog joke like you are a dog and dont write anything else and dont explain the joke spring.ai.mistralai.api-key=${MISTRAL_API_KEY} barkr.api.secret=${SECRET_KEY:default-secret-key} diff --git a/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java b/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java index 05c0b3b..b038196 100644 --- a/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java +++ b/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java @@ -17,12 +17,10 @@ import org.springframework.cache.CacheManager; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; -import org.storkforge.barkr.domain.IssuedApiKeyService; import org.storkforge.barkr.domain.entity.Account; import org.storkforge.barkr.domain.entity.GoogleAccountApiKeyLink; import org.storkforge.barkr.domain.entity.Post; import org.storkforge.barkr.domain.roles.BarkrRole; -import org.storkforge.barkr.filters.ApiKeyAuthenticationFilter; import org.storkforge.barkr.infrastructure.persistence.AccountRepository; import org.storkforge.barkr.infrastructure.persistence.PostRepository; import org.testcontainers.containers.PostgreSQLContainer; @@ -60,13 +58,6 @@ class WebControllerTest { @Transient private GoogleAccountApiKeyLink googleAccountApiKeyLink; - @Autowired - private IssuedApiKeyService issuedApiKeyService; - - @Autowired - private ApiKeyAuthenticationFilter apiKeyAuthenticationFilter; - - @Autowired private PostRepository postRepository; @@ -96,7 +87,7 @@ void viewAllPostsPage() throws IOException { mockAccount.setUsername("mockAccount"); mockAccount.setBreed("husky"); - mockAccount.setGoogleOidc2Id("4"); + mockAccount.setGoogleOidc2Id("6"); mockAccount.setImage(null); mockAccount.setRoles(new HashSet<>(Set.of(BarkrRole.USER, BarkrRole.PREMIUM))); @@ -110,7 +101,7 @@ void viewAllPostsPage() throws IOException { mockAccount2.setUsername("mockAccount2"); mockAccount2.setBreed("beagle"); - mockAccount2.setGoogleOidc2Id("5"); + mockAccount2.setGoogleOidc2Id("7"); mockAccount2.setImage(null); mockAccount2.setRoles(new HashSet<>(Set.of(BarkrRole.USER, BarkrRole.PREMIUM))); From 95f5b37e06ee95d375527f9cc809a7d15fe3beed Mon Sep 17 00:00:00 2001 From: efpcode Date: Wed, 23 Apr 2025 12:29:21 +0200 Subject: [PATCH 44/51] Adds env vars to ci.yaml --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4fbe98..1020a34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,14 @@ jobs: java-version: 24 cache: 'maven' + - name: Set environment variables + env: + MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} + SECRET_KEY: ${{ secrets.SECRET_KEY }} + GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} + GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} + run: echo "Environment variables set" + - name: Compile with Maven run: mvn -B --no-transfer-progress compile --file pom.xml From 5c0ff5a43b9a39757b8a020054b2863d9a74df2f Mon Sep 17 00:00:00 2001 From: efpcode Date: Wed, 23 Apr 2025 12:48:43 +0200 Subject: [PATCH 45/51] Adds depency to webcontrolltest --- .../barkr/web/controller/WebControllerTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java b/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java index b038196..721dd54 100644 --- a/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java +++ b/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java @@ -7,6 +7,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -17,6 +19,8 @@ import org.springframework.cache.CacheManager; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; +import org.storkforge.barkr.api.controller.ApiKeyController; +import org.storkforge.barkr.domain.IssuedApiKeyService; import org.storkforge.barkr.domain.entity.Account; import org.storkforge.barkr.domain.entity.GoogleAccountApiKeyLink; import org.storkforge.barkr.domain.entity.Post; @@ -52,6 +56,13 @@ class WebControllerTest { @ServiceConnection static RedisContainer redis = new RedisContainer(DockerImageName.parse("redis:latest")); + @Mock + private IssuedApiKeyService issuedApiKeyService; + + @InjectMocks + private ApiKeyController apiKeyController; + + @Autowired private AccountRepository accountRepository; From f39b682ae3c6404a92408d89c494e9436fd0e6e4 Mon Sep 17 00:00:00 2001 From: efpcode Date: Wed, 23 Apr 2025 17:13:34 +0200 Subject: [PATCH 46/51] (fix) Circular dependency between account issuedapikey service + all test passing --- .../api/controller/ApiKeyController.java | 15 ++-- .../barkr/domain/IssuedApiKeyService.java | 35 +++++---- .../GoogleAccountApiKeyLinkRepository.java | 4 +- .../persistence/IssuedApiKeyRepository.java | 3 + .../api/controller/ApiKeyControllerTest.java | 71 +++++++++++++++---- 5 files changed, 88 insertions(+), 40 deletions(-) diff --git a/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java b/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java index 7fdd88e..701a33b 100644 --- a/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java +++ b/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java @@ -12,7 +12,6 @@ import org.storkforge.barkr.dto.apiKeyDto.GenerateApiKeyRequest; import org.storkforge.barkr.dto.apiKeyDto.ResponseApiKeyOnce; import org.storkforge.barkr.dto.apiKeyDto.UpdateApiKey; -import org.storkforge.barkr.infrastructure.persistence.AccountRepository; import org.storkforge.barkr.mapper.ApiKeyMapper; import java.security.InvalidKeyException; @@ -27,11 +26,9 @@ @RequestMapping("/apikeys") public class ApiKeyController { private final IssuedApiKeyService issuedApiKeyService; - private final AccountRepository accountRepository; - public ApiKeyController(IssuedApiKeyService issuedApiKeyService, AccountRepository accountRepository) { + public ApiKeyController(IssuedApiKeyService issuedApiKeyService) { this.issuedApiKeyService = issuedApiKeyService; - this.accountRepository = accountRepository; } @GetMapping("/apikeyform") @@ -88,18 +85,20 @@ public String generateApiKey( @GetMapping("/mykeys") public String myKeys(Model model , @AuthenticationPrincipal OidcUser user) { - var currentUser = accountRepository.findByGoogleOidc2Id(user.getName()); - var keys = issuedApiKeyService.allApiKeys(); + var currentUser = issuedApiKeyService.findByGoogleOidc2Id(user.getName()); + var keys = issuedApiKeyService.allApiKeys(user.getName()); + model.addAttribute("account", currentUser.get().getAccount()); model.addAttribute("keys", keys.apiKeys()); - model.addAttribute("account", currentUser.get()); return "apikeys/mykeys"; } @PostMapping("/mykeys/revoke") - public String revokeKey(@RequestParam String referenceId) { + public String revokeKey(@RequestParam String referenceId, @AuthenticationPrincipal OidcUser user) { + if (issuedApiKeyService.isValidUuid(UUID.fromString(referenceId), user.getName())) { var update = new UpdateApiKey(UUID.fromString(referenceId),null, true, null); issuedApiKeyService.updateApiKey(update); + } return "redirect:/apikeys/mykeys"; } diff --git a/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java b/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java index 0733aee..285acfe 100644 --- a/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java +++ b/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java @@ -4,17 +4,19 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.storkforge.barkr.domain.entity.GoogleAccountApiKeyLink; import org.storkforge.barkr.domain.entity.IssuedApiKey; import org.storkforge.barkr.dto.apiKeyDto.CreateApiKey; import org.storkforge.barkr.dto.apiKeyDto.GenerateApiKeyRequest; import org.storkforge.barkr.dto.apiKeyDto.ResponseApiKeyList; import org.storkforge.barkr.dto.apiKeyDto.UpdateApiKey; +import org.storkforge.barkr.exceptions.AccountNotFound; import org.storkforge.barkr.exceptions.IssuedApiKeyNotFound; -import org.storkforge.barkr.infrastructure.persistence.AccountRepository; import org.storkforge.barkr.infrastructure.persistence.GoogleAccountApiKeyLinkRepository; import org.storkforge.barkr.infrastructure.persistence.IssuedApiKeyRepository; import org.storkforge.barkr.mapper.ApiKeyMapper; @@ -28,6 +30,7 @@ import java.util.Base64; import java.util.Optional; import java.util.UUID; +import java.util.stream.Collectors; @Service @Transactional @@ -37,18 +40,20 @@ public class IssuedApiKeyService { private final IssuedApiKeyRepository issuedApiKeyRepository; private final GoogleAccountApiKeyLinkRepository googleAccountApiKeyLinkRepository; - private final AccountRepository accountRepository; @Value("${barkr.api.secret}") private String secretKey; public IssuedApiKeyService( IssuedApiKeyRepository issuedApiKeyRepository, - GoogleAccountApiKeyLinkRepository googleAccountApiKeyLinkRepository, - AccountRepository accountRepository) { + GoogleAccountApiKeyLinkRepository googleAccountApiKeyLinkRepository) { this.issuedApiKeyRepository = issuedApiKeyRepository; this.googleAccountApiKeyLinkRepository = googleAccountApiKeyLinkRepository; - this.accountRepository = accountRepository; + + } + + public Optional findByGoogleOidc2Id(String name) { + return Optional.ofNullable(googleAccountApiKeyLinkRepository.findByAccount_GoogleOidc2Id(name).orElseThrow(() -> new AccountNotFound("Account was not found"))); } @@ -62,8 +67,7 @@ public void apiKeyGenerate(GenerateApiKeyRequest request, String hashedApiKey) { SecurityContext securityContext = SecurityContextHolder.getContext(); Authentication authentication = securityContext.getAuthentication(); var googleOidc2id = authentication.getName(); - var account = accountRepository.findByGoogleOidc2Id(googleOidc2id); - var link = googleAccountApiKeyLinkRepository.findByAccount(account.get()); + var link = googleAccountApiKeyLinkRepository.findByAccount_GoogleOidc2Id(googleOidc2id).orElseThrow(() -> new AccountNotFound("Account was not found! ")); LocalDateTime issueDate = LocalDateTime.now(); CreateApiKey createApiKey = new CreateApiKey( @@ -73,7 +77,7 @@ public void apiKeyGenerate(GenerateApiKeyRequest request, String hashedApiKey) { false, issueDate, request.apiKeyName(), - link.get(), + link, generateUuid() ); @@ -129,16 +133,12 @@ public boolean apiKeyValidation(String rawApiKey) throws NoSuchAlgorithmExceptio } - public ResponseApiKeyList allApiKeys() { - SecurityContext securityContext = SecurityContextHolder.getContext(); - Authentication authentication = securityContext.getAuthentication(); - var googleOidc2id = authentication.getName(); + public ResponseApiKeyList allApiKeys(String googleOidc2Id) { var recordsRemoved = revokeExpiredApiKeys(); log.info("Removed " + recordsRemoved + " api keys"); - var account = accountRepository.findByGoogleOidc2Id(googleOidc2id); - var link = googleAccountApiKeyLinkRepository.findByAccount(account.get()); + var link = googleAccountApiKeyLinkRepository.findByAccount_GoogleOidc2Id(googleOidc2Id); var apiKeys = issuedApiKeyRepository.findByGoogleAccountApiKeyLinkOrderByIssuedAtDesc(link.get()); var apiKeyResponse = apiKeys.stream().map(ApiKeyMapper::mapToResponse).toList(); @@ -158,6 +158,13 @@ public int revokeExpiredApiKeys() { return issuedApiKeyRepository.revokeExpiredKeys(LocalDateTime.now()); } + public boolean isValidUuid(UUID uuid, String googleOidc2Id) { + var allApiKeys = allApiKeys(googleOidc2Id); + return allApiKeys.apiKeys().stream() + .anyMatch(k -> k.referenceId().equals(uuid)); + + } + diff --git a/src/main/java/org/storkforge/barkr/infrastructure/persistence/GoogleAccountApiKeyLinkRepository.java b/src/main/java/org/storkforge/barkr/infrastructure/persistence/GoogleAccountApiKeyLinkRepository.java index 9dc5e34..96257dd 100644 --- a/src/main/java/org/storkforge/barkr/infrastructure/persistence/GoogleAccountApiKeyLinkRepository.java +++ b/src/main/java/org/storkforge/barkr/infrastructure/persistence/GoogleAccountApiKeyLinkRepository.java @@ -1,8 +1,6 @@ package org.storkforge.barkr.infrastructure.persistence; -import jakarta.validation.constraints.NotNull; import org.springframework.data.jpa.repository.JpaRepository; -import org.storkforge.barkr.domain.entity.Account; import org.storkforge.barkr.domain.entity.GoogleAccountApiKeyLink; import java.util.Optional; @@ -10,6 +8,6 @@ public interface GoogleAccountApiKeyLinkRepository extends JpaRepository { @Override Optional findById(Long id); + Optional findByAccount_GoogleOidc2Id(String googleOidc2id); - Optional findByAccount(@NotNull Account account); } diff --git a/src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java b/src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java index 9667f79..0b8790e 100644 --- a/src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java +++ b/src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java @@ -26,4 +26,7 @@ public interface IssuedApiKeyRepository extends JpaRepository Date: Wed, 23 Apr 2025 21:16:42 +0200 Subject: [PATCH 47/51] Adds a bit of safety add all test are passing --- .../api/controller/ApiKeyController.java | 4 +- .../barkr/domain/IssuedApiKeyService.java | 21 ++++---- .../persistence/IssuedApiKeyRepository.java | 5 -- .../api/controller/ApiKeyControllerTest.java | 14 +++-- .../barkr/domain/IssuedApiKeyServiceTest.java | 53 +++++++++++++++++++ .../web/controller/WebControllerTest.java | 2 +- 6 files changed, 79 insertions(+), 20 deletions(-) create mode 100644 src/test/java/org/storkforge/barkr/domain/IssuedApiKeyServiceTest.java diff --git a/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java b/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java index 701a33b..c54544e 100644 --- a/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java +++ b/src/main/java/org/storkforge/barkr/api/controller/ApiKeyController.java @@ -103,9 +103,11 @@ public String revokeKey(@RequestParam String referenceId, @AuthenticationPrincip } @PostMapping("mykeys/nameupdate") - public String nameUpdate(@RequestParam String apiKeyName, @RequestParam String referenceId) { + public String nameUpdate(@RequestParam String apiKeyName, @RequestParam String referenceId, @AuthenticationPrincipal OidcUser user) { + if(issuedApiKeyService.isValidUuid(UUID.fromString(referenceId), user.getName())) { var update = new UpdateApiKey(UUID.fromString(referenceId), apiKeyName, false, null); issuedApiKeyService.updateApiKey(update); + } return "redirect:/apikeys/mykeys"; } diff --git a/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java b/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java index 285acfe..fc129de 100644 --- a/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java +++ b/src/main/java/org/storkforge/barkr/domain/IssuedApiKeyService.java @@ -2,9 +2,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; -import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; @@ -30,7 +30,6 @@ import java.util.Base64; import java.util.Optional; import java.util.UUID; -import java.util.stream.Collectors; @Service @Transactional @@ -44,6 +43,7 @@ public class IssuedApiKeyService { private String secretKey; + @Autowired public IssuedApiKeyService( IssuedApiKeyRepository issuedApiKeyRepository, GoogleAccountApiKeyLinkRepository googleAccountApiKeyLinkRepository) { @@ -82,7 +82,11 @@ public void apiKeyGenerate(GenerateApiKeyRequest request, String hashedApiKey) { ); log.info("Adding new api key to account"); - issuedApiKeyRepository.save(ApiKeyMapper.mapToEntity(createApiKey)); + + IssuedApiKey key = ApiKeyMapper.mapToEntity(createApiKey); + + if(key != null) + issuedApiKeyRepository.save(key); } @@ -126,20 +130,17 @@ public boolean apiKeyExists(String hashedApiKey) { public boolean apiKeyValidation(String rawApiKey) throws NoSuchAlgorithmException, InvalidKeyException { var hashedApikey = hashedApiKey(rawApiKey); var keyFound = issuedApiKeyRepository.findByHashedApiKey(hashedApikey); - if (keyFound.isPresent() && !keyFound.get().isRevoked()) { - return true; - } - return false; + return keyFound.isPresent() && !keyFound.get().isRevoked(); } public ResponseApiKeyList allApiKeys(String googleOidc2Id) { var recordsRemoved = revokeExpiredApiKeys(); - log.info("Removed " + recordsRemoved + " api keys"); + log.info("Removed {} api keys", recordsRemoved); - var link = googleAccountApiKeyLinkRepository.findByAccount_GoogleOidc2Id(googleOidc2Id); - var apiKeys = issuedApiKeyRepository.findByGoogleAccountApiKeyLinkOrderByIssuedAtDesc(link.get()); + var link = googleAccountApiKeyLinkRepository.findByAccount_GoogleOidc2Id(googleOidc2Id).orElseThrow(() -> new AccountNotFound("Account was not found! ")); + var apiKeys = issuedApiKeyRepository.findByGoogleAccountApiKeyLinkOrderByIssuedAtDesc(link); var apiKeyResponse = apiKeys.stream().map(ApiKeyMapper::mapToResponse).toList(); return new ResponseApiKeyList(apiKeyResponse); diff --git a/src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java b/src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java index 0b8790e..19e3d16 100644 --- a/src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java +++ b/src/main/java/org/storkforge/barkr/infrastructure/persistence/IssuedApiKeyRepository.java @@ -13,8 +13,6 @@ import java.util.UUID; public interface IssuedApiKeyRepository extends JpaRepository { - @Override - Optional findById(Long id); Optional findByHashedApiKey(String hashedApiKey); @@ -26,7 +24,4 @@ public interface IssuedApiKeyRepository extends JpaRepository postgres = new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest")); + + @Container + @ServiceConnection + static RedisContainer redis = new RedisContainer(DockerImageName.parse("redis:latest")); + + @MockitoBean + private IssuedApiKeyRepository issuedApiKeyRepository; + + @Value("${barkr.api.secret}") + private String secretKey; + + @MockitoBean + private GoogleAccountApiKeyLinkRepository googleAccountApiKeyLinkRepository; + + @Autowired + private IssuedApiKeyService service; + + + @Test + @DisplayName("testValue injection is working") + void testValueInjectionIsWorking() { + System.out.println(secretKey); + assertThat(secretKey).isEqualTo("test-value"); + + } + +} diff --git a/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java b/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java index 721dd54..5b2f689 100644 --- a/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java +++ b/src/test/java/org/storkforge/barkr/web/controller/WebControllerTest.java @@ -66,7 +66,7 @@ class WebControllerTest { @Autowired private AccountRepository accountRepository; - @Transient + @Mock private GoogleAccountApiKeyLink googleAccountApiKeyLink; @Autowired From 54368e1913fc88a432dd99567d24732e3611fd3f Mon Sep 17 00:00:00 2001 From: efpcode Date: Wed, 23 Apr 2025 21:29:00 +0200 Subject: [PATCH 48/51] Adds new injection to apikeycontroll test --- .../api/controller/ApiKeyControllerTest.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/test/java/org/storkforge/barkr/api/controller/ApiKeyControllerTest.java b/src/test/java/org/storkforge/barkr/api/controller/ApiKeyControllerTest.java index b7fe3cd..e199923 100644 --- a/src/test/java/org/storkforge/barkr/api/controller/ApiKeyControllerTest.java +++ b/src/test/java/org/storkforge/barkr/api/controller/ApiKeyControllerTest.java @@ -16,12 +16,15 @@ import org.storkforge.barkr.domain.IssuedApiKeyService; import org.storkforge.barkr.domain.entity.Account; import org.storkforge.barkr.domain.entity.GoogleAccountApiKeyLink; +import org.storkforge.barkr.dto.apiKeyDto.GenerateApiKeyRequest; import org.storkforge.barkr.dto.apiKeyDto.ResponseApiKeyList; import org.storkforge.barkr.dto.apiKeyDto.ResponseApiKeyOnce; import org.storkforge.barkr.dto.apiKeyDto.UpdateApiKey; import org.storkforge.barkr.infrastructure.persistence.AccountRepository; import org.storkforge.barkr.infrastructure.persistence.GoogleAccountApiKeyLinkRepository; +import org.storkforge.barkr.infrastructure.persistence.IssuedApiKeyRepository; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -46,6 +49,9 @@ class ApiKeyControllerTest { @MockitoBean private IssuedApiKeyService issuedApiKeyService; + @MockitoBean + private IssuedApiKeyRepository issuedApiKeyRepository; + @MockitoBean private GoogleAccountApiKeyLinkRepository googleAccountApiKeyLinkRepository; @@ -75,6 +81,26 @@ void showApiKeyForm() throws Exception { .andExpect(view().name("apikeys")); } + + @Test + @WithMockUser(username = "testUser", roles = {"USER"}) + void testGenerateApiKey() throws Exception { + GenerateApiKeyRequest request = new GenerateApiKeyRequest("TestKey", LocalDateTime.now().plusDays(1)); + String rawApiKey = "mockRawApiKey"; + String hashedApiKey = "mockHashedApiKey"; + + when(issuedApiKeyService.generateRawApiKey()).thenReturn(rawApiKey); + when(issuedApiKeyService.hashedApiKey(rawApiKey)).thenReturn(hashedApiKey); + when(issuedApiKeyService.apiKeyExists(hashedApiKey)).thenReturn(false); + + mvc.perform(post("/apikeys/generate").with(csrf()) + .param("apiKeyName", request.apiKeyName()) + .param("expiresAt", request.expiresAt().toString())) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/apikeys/result")); + } + + @Test @DisplayName("GET /apikeys/result redirects if response is null") void showGeneratedApiKey_redirectsWhenResponseIsNull() throws Exception { From 448f45da19ef826a24ad1bf681fb85e834ce4710 Mon Sep 17 00:00:00 2001 From: efpcode Date: Wed, 23 Apr 2025 21:58:58 +0200 Subject: [PATCH 49/51] Adds version log to ci workflow --- .github/workflows/ci.yml | 6 ++++++ .../barkr/api/controller/ApiKeyControllerTest.java | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1020a34..a0e52ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,12 @@ jobs: runs-on: ubuntu-latest steps: + - name: Check Maven version + run: mvn --version + + - name: Check Java version + run: java -version + - name: Checkout uses: actions/checkout@v4 diff --git a/src/test/java/org/storkforge/barkr/api/controller/ApiKeyControllerTest.java b/src/test/java/org/storkforge/barkr/api/controller/ApiKeyControllerTest.java index e199923..97e52f6 100644 --- a/src/test/java/org/storkforge/barkr/api/controller/ApiKeyControllerTest.java +++ b/src/test/java/org/storkforge/barkr/api/controller/ApiKeyControllerTest.java @@ -1,5 +1,6 @@ package org.storkforge.barkr.api.controller; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -24,6 +25,8 @@ import org.storkforge.barkr.infrastructure.persistence.GoogleAccountApiKeyLinkRepository; import org.storkforge.barkr.infrastructure.persistence.IssuedApiKeyRepository; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -73,6 +76,17 @@ private static org.springframework.test.web.servlet.request.MockHttpServletReque return post(url).with(csrf()); } + + + @BeforeEach + void setupMocks() throws NoSuchAlgorithmException, InvalidKeyException { + // Define behavior for issuedApiKeyService mock + when(issuedApiKeyService.generateRawApiKey()).thenReturn("mockRawApiKey"); + when(issuedApiKeyService.hashedApiKey("mockRawApiKey")).thenReturn("mockHashedApiKey"); + when(issuedApiKeyService.apiKeyExists("mockHashedApiKey")).thenReturn(false); + } + + @Test @DisplayName("GET /apikeys/apikeyform returns form view") void showApiKeyForm() throws Exception { From 77d8972a0ba308bda08c635e6f7f9f93ad2b8214 Mon Sep 17 00:00:00 2001 From: efpcode Date: Wed, 23 Apr 2025 22:09:30 +0200 Subject: [PATCH 50/51] (fix) Adds logs to ci version --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0e52ee..522c43e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,12 +11,6 @@ jobs: runs-on: ubuntu-latest steps: - - name: Check Maven version - run: mvn --version - - - name: Check Java version - run: java -version - - name: Checkout uses: actions/checkout@v4 @@ -27,6 +21,12 @@ jobs: java-version: 24 cache: 'maven' + - name: Check Maven version + run: mvn --version + + - name: Check Java version + run: java -version + - name: Set environment variables env: MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} From 3f8bd1177d10a7a630c71ebd03701569bd689d54 Mon Sep 17 00:00:00 2001 From: efpcode Date: Wed, 23 Apr 2025 22:26:10 +0200 Subject: [PATCH 51/51] Adds set env vars as the first layer --- .github/workflows/ci.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 522c43e..9e08bcb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,11 @@ on: jobs: builds: runs-on: ubuntu-latest + env: + MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} + SECRET_KEY: ${{ secrets.SECRET_KEY }} + GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} + GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} steps: - name: Checkout @@ -27,13 +32,7 @@ jobs: - name: Check Java version run: java -version - - name: Set environment variables - env: - MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} - SECRET_KEY: ${{ secrets.SECRET_KEY }} - GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} - GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} - run: echo "Environment variables set" + - name: Compile with Maven run: mvn -B --no-transfer-progress compile --file pom.xml