From f26a9a69f76130596ebf481749dbbed23889ff78 Mon Sep 17 00:00:00 2001 From: esgoet Date: Wed, 21 Aug 2024 11:38:43 +0200 Subject: [PATCH 1/8] Added login functionality --- backend/pom.xml | 4 +++ .../backend/controllers/AuthController.java | 16 +++++++++ .../backend/security/SecurityConfig.java | 36 +++++++++++++++++++ .../src/main/resources/application.properties | 4 +++ frontend/src/App.tsx | 16 ++++++++- .../src/components/navigation/Navigation.tsx | 4 +++ .../DashboardPage/dashboard/Dashboard.tsx | 15 ++++++-- .../pages/LoginPage/loginPage/LoginPage.tsx | 12 +++++++ 8 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/java/com/github/esgoet/backend/controllers/AuthController.java create mode 100644 backend/src/main/java/com/github/esgoet/backend/security/SecurityConfig.java create mode 100644 frontend/src/pages/LoginPage/loginPage/LoginPage.tsx diff --git a/backend/pom.xml b/backend/pom.xml index f803a5c..c22e211 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -58,6 +58,10 @@ 4.13.0 test + + org.springframework.boot + spring-boot-starter-oauth2-client + diff --git a/backend/src/main/java/com/github/esgoet/backend/controllers/AuthController.java b/backend/src/main/java/com/github/esgoet/backend/controllers/AuthController.java new file mode 100644 index 0000000..33179de --- /dev/null +++ b/backend/src/main/java/com/github/esgoet/backend/controllers/AuthController.java @@ -0,0 +1,16 @@ +package com.github.esgoet.backend.controllers; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/users") +public class AuthController { + @GetMapping("/me") + public String getUser(@AuthenticationPrincipal OAuth2User user) { + return user.getAttributes().get("login").toString(); + } +} diff --git a/backend/src/main/java/com/github/esgoet/backend/security/SecurityConfig.java b/backend/src/main/java/com/github/esgoet/backend/security/SecurityConfig.java new file mode 100644 index 0000000..a1868f0 --- /dev/null +++ b/backend/src/main/java/com/github/esgoet/backend/security/SecurityConfig.java @@ -0,0 +1,36 @@ +package com.github.esgoet.backend.security; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Value("${app.url}") + private String appUrl; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(a -> a +// .requestMatchers("/api/books").authenticated() +// .requestMatchers("/api/books/**").authenticated() + .anyRequest().permitAll() + ) + .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)) + .exceptionHandling(e -> e + .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))) + .oauth2Login(o -> o.defaultSuccessUrl(appUrl)); + return http.build(); + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 7f7db72..51c4040 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1,2 +1,6 @@ spring.application.name=backend spring.data.mongodb.uri=${MONGO_DB_URI} +spring.security.oauth2.client.registration.github.client-id=${GITHUB_ID} +spring.security.oauth2.client.registration.github.client-secret=${GITHUB_SECRET} +spring.security.oauth2.client.registration.github.scope=none +app.url=${APP_URL} \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7a1ecc7..121c0e5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,10 +9,12 @@ import AddBookForm from "./pages/BookGalleryPage/components/addBookForm/AddBookF import Header from "./components/header/Header.tsx"; import Navigation from "./components/navigation/Navigation.tsx"; import Dashboard from "./pages/DashboardPage/dashboard/Dashboard.tsx"; +import LoginPage from "./pages/LoginPage/loginPage/LoginPage.tsx"; export default function App() { const [data, setData] = useState([]) + const [user, setUser] = useState(null); const fetchBooks = () => { axios.get("/api/books") @@ -45,14 +47,26 @@ export default function App() { const filteredBooks: Book[] = data .filter((book) => book.title?.toLowerCase().includes(searchInput.toLowerCase()) || book.author?.toLowerCase().includes(searchInput.toLowerCase())); + + const login = () => { + const host = window.location.host === 'localhost:5173' ? 'http://localhost:8080': window.location.origin + window.open(host + '/oauth2/authorization/github', '_self') + } + + const loadUser = () => { + axios.get("/api/users/me") + .then((response) => setUser(response.data)) + } return ( <>
+
- }/> + }/> + }/> }/> diff --git a/frontend/src/components/navigation/Navigation.tsx b/frontend/src/components/navigation/Navigation.tsx index 6d08186..50c7e1b 100644 --- a/frontend/src/components/navigation/Navigation.tsx +++ b/frontend/src/components/navigation/Navigation.tsx @@ -14,6 +14,10 @@ auto_stories
  • add_circle Add Book
  • +
  • +login +Login
  • + ) diff --git a/frontend/src/pages/DashboardPage/dashboard/Dashboard.tsx b/frontend/src/pages/DashboardPage/dashboard/Dashboard.tsx index 8b62ce1..2f9389c 100644 --- a/frontend/src/pages/DashboardPage/dashboard/Dashboard.tsx +++ b/frontend/src/pages/DashboardPage/dashboard/Dashboard.tsx @@ -1,6 +1,17 @@ +import {useEffect} from "react"; + +type DashboardProps = { + user: string, + loadUser: () => void; +} +export default function Dashboard({user, loadUser}:DashboardProps) { + useEffect(() => { + loadUser() + },[]) -export default function Dashboard() { return ( -
    +
    + {user &&

    Hello {user}

    } +
    ); } diff --git a/frontend/src/pages/LoginPage/loginPage/LoginPage.tsx b/frontend/src/pages/LoginPage/loginPage/LoginPage.tsx new file mode 100644 index 0000000..a1624c3 --- /dev/null +++ b/frontend/src/pages/LoginPage/loginPage/LoginPage.tsx @@ -0,0 +1,12 @@ +type LoginPageProps = { + login: () => void; +} + +export default function LoginPage({login}: LoginPageProps) { + return ( + <> +

    Login

    + + + ) +} \ No newline at end of file From afad1a34632d98024ac8d3afa0b514cb24aceea7 Mon Sep 17 00:00:00 2001 From: esgoet Date: Wed, 21 Aug 2024 11:39:34 +0200 Subject: [PATCH 2/8] Fix user state initial value --- frontend/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 121c0e5..dd6b818 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,7 +14,7 @@ import LoginPage from "./pages/LoginPage/loginPage/LoginPage.tsx"; export default function App() { const [data, setData] = useState([]) - const [user, setUser] = useState(null); + const [user, setUser] = useState(""); const fetchBooks = () => { axios.get("/api/books") From d952b7ddc14fcc615cd08b5b2cf7b7f6b4fe045e Mon Sep 17 00:00:00 2001 From: esgoet Date: Fri, 23 Aug 2024 15:47:36 +0200 Subject: [PATCH 3/8] Add logout, authcontroller integration test and mock user to other integration test --- backend/pom.xml | 5 +++ .../AuthController.java | 4 +-- .../backend/security/SecurityConfig.java | 3 +- .../BookControllerIntegrationTest.java | 6 ++++ .../backend/security/AuthControllerTest.java | 32 +++++++++++++++++++ .../src/test/resources/application.properties | 6 +++- frontend/src/App.tsx | 21 +++++++++--- 7 files changed, 68 insertions(+), 9 deletions(-) rename backend/src/main/java/com/github/esgoet/backend/{book/controllers => security}/AuthController.java (87%) create mode 100644 backend/src/test/java/com/github/esgoet/backend/security/AuthControllerTest.java diff --git a/backend/pom.xml b/backend/pom.xml index c22e211..6cedaf2 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -62,6 +62,11 @@ org.springframework.boot spring-boot-starter-oauth2-client + + org.springframework.security + spring-security-test + test + diff --git a/backend/src/main/java/com/github/esgoet/backend/book/controllers/AuthController.java b/backend/src/main/java/com/github/esgoet/backend/security/AuthController.java similarity index 87% rename from backend/src/main/java/com/github/esgoet/backend/book/controllers/AuthController.java rename to backend/src/main/java/com/github/esgoet/backend/security/AuthController.java index 33179de..2cbc838 100644 --- a/backend/src/main/java/com/github/esgoet/backend/book/controllers/AuthController.java +++ b/backend/src/main/java/com/github/esgoet/backend/security/AuthController.java @@ -1,4 +1,4 @@ -package com.github.esgoet.backend.controllers; +package com.github.esgoet.backend.security; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.user.OAuth2User; @@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/users") +@RequestMapping("/api/auth") public class AuthController { @GetMapping("/me") public String getUser(@AuthenticationPrincipal OAuth2User user) { diff --git a/backend/src/main/java/com/github/esgoet/backend/security/SecurityConfig.java b/backend/src/main/java/com/github/esgoet/backend/security/SecurityConfig.java index a1868f0..0a3ab80 100644 --- a/backend/src/main/java/com/github/esgoet/backend/security/SecurityConfig.java +++ b/backend/src/main/java/com/github/esgoet/backend/security/SecurityConfig.java @@ -30,7 +30,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)) .exceptionHandling(e -> e .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))) - .oauth2Login(o -> o.defaultSuccessUrl(appUrl)); + .oauth2Login(o -> o.defaultSuccessUrl(appUrl)) + .logout(l -> l.logoutSuccessUrl(appUrl)); return http.build(); } } diff --git a/backend/src/test/java/com/github/esgoet/backend/book/controllers/BookControllerIntegrationTest.java b/backend/src/test/java/com/github/esgoet/backend/book/controllers/BookControllerIntegrationTest.java index b3a52ae..aa0fe50 100644 --- a/backend/src/test/java/com/github/esgoet/backend/book/controllers/BookControllerIntegrationTest.java +++ b/backend/src/test/java/com/github/esgoet/backend/book/controllers/BookControllerIntegrationTest.java @@ -9,6 +9,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.web.servlet.MockMvc; @@ -33,6 +34,7 @@ class BookControllerIntegrationTest { private final LocalDate createdDate = LocalDate.parse("2024-08-22"); @Test + @WithMockUser void getAllBooks_Test_When_DbEmpty_Then_returnEmptyArray() throws Exception { mockMvc.perform(get("/api/books")) @@ -43,6 +45,7 @@ void getAllBooks_Test_When_DbEmpty_Then_returnEmptyArray() throws Exception { @DirtiesContext @Test + @WithMockUser void getBook_Test_whenIdExists() throws Exception { //GIVEN bookRepository.save(new Book("1", "George Orwell", "1984", Genre.THRILLER, "this is a description", "123456isbn", "https://linkToCover", 3,localDate, ReadingStatus.TO_BE_READ, createdDate)); @@ -69,6 +72,7 @@ void getBook_Test_whenIdExists() throws Exception { @Test @DirtiesContext + @WithMockUser void addABookTest_whenNewBookExists_thenReturnNewBook() throws Exception { // GIVEN @@ -105,6 +109,7 @@ void addABookTest_whenNewBookExists_thenReturnNewBook() throws Exception { @DirtiesContext @Test + @WithMockUser void getBook_Test_whenIdDoesNotExists() throws Exception { //WHEN mockMvc.perform(get("/api/books/1")) @@ -120,6 +125,7 @@ void getBook_Test_whenIdDoesNotExists() throws Exception { } @Test + @WithMockUser void deleteBook() throws Exception { bookRepository.save(new Book("1", "Simon", "HowToDeleteBooksFast", Genre.SCIENCE, "description", "12345678", "https://linkToCover", 3,localDate, ReadingStatus.TO_BE_READ, createdDate)); diff --git a/backend/src/test/java/com/github/esgoet/backend/security/AuthControllerTest.java b/backend/src/test/java/com/github/esgoet/backend/security/AuthControllerTest.java new file mode 100644 index 0000000..50edf88 --- /dev/null +++ b/backend/src/test/java/com/github/esgoet/backend/security/AuthControllerTest.java @@ -0,0 +1,32 @@ +package com.github.esgoet.backend.security; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class AuthControllerTest { + @Autowired + MockMvc mockMvc; + + @Test + @DirtiesContext + void getLoggedInUserTest() throws Exception { + mockMvc.perform(get("/api/auth/me") + .with(oidcLogin().idToken(token -> token.subject("123")) + .userInfoToken(token -> token.claim("login", "esgoet")))) + .andExpect(status().isOk()) + .andExpect(content().string("esgoet")); + } + + +} diff --git a/backend/src/test/resources/application.properties b/backend/src/test/resources/application.properties index b0360a6..8543d2f 100644 --- a/backend/src/test/resources/application.properties +++ b/backend/src/test/resources/application.properties @@ -1,2 +1,6 @@ spring.application.name=backend -de.flapdoodle.mongodb.embedded.version=7.0.4 \ No newline at end of file +de.flapdoodle.mongodb.embedded.version=7.0.4 +spring.security.oauth2.client.registration.github.client-id=test-id +spring.security.oauth2.client.registration.github.client-secret=test-secret +spring.security.oauth2.client.registration.github.scope=none +app.url=http://localhost \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 86755b4..6efdf02 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -67,23 +67,34 @@ export default function App() { const host = window.location.host === 'localhost:5173' ? 'http://localhost:8080': window.location.origin window.open(host + '/oauth2/authorization/github', '_self') } + const logout = () => { + const host = window.location.host === 'localhost:5173' ? 'http://localhost:8080': window.location.origin + window.open(host + "/logout", "_self") + } const loadUser = () => { - axios.get("/api/users/me") - .then((response) => setUser(response.data)) + axios.get("/api/auth/me") + .then((response) => console.log(response.data)) + .catch((error) => console.log(error)) } return ( <>
    + +
    }/> }/> - }/> - }/> - }/> + }/> + }/> + }/> }/>
    From 43407420d4cd3b2f9def2c6a8c6eb92245b2d7ae Mon Sep 17 00:00:00 2001 From: esgoet Date: Fri, 23 Aug 2024 15:48:51 +0200 Subject: [PATCH 4/8] Add mock user to other integration test --- .../user/controllers/UserControllerIntegrationTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backend/src/test/java/com/github/esgoet/backend/user/controllers/UserControllerIntegrationTest.java b/backend/src/test/java/com/github/esgoet/backend/user/controllers/UserControllerIntegrationTest.java index 2baeacb..e0ec3e6 100644 --- a/backend/src/test/java/com/github/esgoet/backend/user/controllers/UserControllerIntegrationTest.java +++ b/backend/src/test/java/com/github/esgoet/backend/user/controllers/UserControllerIntegrationTest.java @@ -7,6 +7,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.web.servlet.MockMvc; @@ -26,6 +27,7 @@ class UserControllerIntegrationTest { private final LocalDate goalDate = LocalDate.parse("2024-12-31"); @Test + @WithMockUser void getUsersTest() throws Exception { mockMvc.perform(get("/api/users")) .andExpect(status().isOk()) @@ -34,6 +36,7 @@ void getUsersTest() throws Exception { @DirtiesContext @Test + @WithMockUser void getUserByIdTest_whenIdExists() throws Exception{ //GIVEN userRepository.save(new User("1","esgoet", 6, goalDate, 0)); @@ -53,6 +56,7 @@ void getUserByIdTest_whenIdExists() throws Exception{ } @Test + @WithMockUser void getUserByIdTest_whenIdDoesNotExist() throws Exception { //WHEN mockMvc.perform(get("/api/users/1")) @@ -69,6 +73,7 @@ void getUserByIdTest_whenIdDoesNotExist() throws Exception { @DirtiesContext @Test + @WithMockUser void addUserTest() throws Exception { //WHEN mockMvc.perform(post("/api/users") @@ -95,6 +100,7 @@ void addUserTest() throws Exception { @DirtiesContext @Test + @WithMockUser void updateUserTest() throws Exception { //GIVEN userRepository.save(new User("1","esgoet", 6, goalDate, 0)); @@ -125,6 +131,7 @@ void updateUserTest() throws Exception { @DirtiesContext @Test + @WithMockUser void deleteUserTest() throws Exception { //GIVEN userRepository.save(new User("1","esgoet", 6, goalDate, 0)); From 64fc4caddfb788a8cae247aee2ac6369efb4a191 Mon Sep 17 00:00:00 2001 From: esgoet Date: Fri, 23 Aug 2024 16:24:01 +0200 Subject: [PATCH 5/8] Add logged in user to database --- .../backend/security/AuthController.java | 10 ++++- .../backend/security/SecurityConfig.java | 38 +++++++++++++++++++ .../esgoet/backend/user/dto/UserDto.java | 4 +- .../esgoet/backend/user/models/User.java | 4 +- .../user/repositories/UserRepository.java | 1 + .../backend/user/services/UserService.java | 6 ++- .../backend/security/AuthControllerTest.java | 2 - 7 files changed, 58 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/com/github/esgoet/backend/security/AuthController.java b/backend/src/main/java/com/github/esgoet/backend/security/AuthController.java index 2cbc838..929d92d 100644 --- a/backend/src/main/java/com/github/esgoet/backend/security/AuthController.java +++ b/backend/src/main/java/com/github/esgoet/backend/security/AuthController.java @@ -1,5 +1,8 @@ package com.github.esgoet.backend.security; +import com.github.esgoet.backend.user.models.User; +import com.github.esgoet.backend.user.services.UserService; +import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.web.bind.annotation.GetMapping; @@ -8,9 +11,12 @@ @RestController @RequestMapping("/api/auth") +@RequiredArgsConstructor public class AuthController { + private final UserService userService; + @GetMapping("/me") - public String getUser(@AuthenticationPrincipal OAuth2User user) { - return user.getAttributes().get("login").toString(); + public User getUser(@AuthenticationPrincipal OAuth2User user) { + return userService.getUserByGitHubId(user.getName()); } } diff --git a/backend/src/main/java/com/github/esgoet/backend/security/SecurityConfig.java b/backend/src/main/java/com/github/esgoet/backend/security/SecurityConfig.java index 0a3ab80..6388087 100644 --- a/backend/src/main/java/com/github/esgoet/backend/security/SecurityConfig.java +++ b/backend/src/main/java/com/github/esgoet/backend/security/SecurityConfig.java @@ -1,5 +1,10 @@ package com.github.esgoet.backend.security; +import com.github.esgoet.backend.user.dto.UserDto; +import com.github.esgoet.backend.user.models.User; +import com.github.esgoet.backend.user.models.UserNotFoundException; +import com.github.esgoet.backend.user.services.UserService; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -8,12 +13,23 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.HttpStatusEntryPoint; +import java.time.LocalDate; +import java.util.List; + @Configuration @EnableWebSecurity +@RequiredArgsConstructor public class SecurityConfig { + private final UserService userService; @Value("${app.url}") private String appUrl; @@ -34,4 +50,26 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .logout(l -> l.logoutSuccessUrl(appUrl)); return http.build(); } + + @Bean + public OAuth2UserService oauth2UserService() { + DefaultOAuth2UserService delegate = new DefaultOAuth2UserService(); + + return request -> { + OAuth2User user = delegate.loadUser(request); + User githubUser; + try { + githubUser = userService.getUserById(user.getName()); + } catch (UserNotFoundException e) { + githubUser = userService.saveUser(new UserDto(user.getAttributes().get("login").toString(), + 0, + LocalDate.now(), + 0, + user.getName(), + "USER")); + } + + return new DefaultOAuth2User(List.of(new SimpleGrantedAuthority(githubUser.role())), user.getAttributes(), "id"); + }; + } } diff --git a/backend/src/main/java/com/github/esgoet/backend/user/dto/UserDto.java b/backend/src/main/java/com/github/esgoet/backend/user/dto/UserDto.java index 1625413..d97cb71 100644 --- a/backend/src/main/java/com/github/esgoet/backend/user/dto/UserDto.java +++ b/backend/src/main/java/com/github/esgoet/backend/user/dto/UserDto.java @@ -9,6 +9,8 @@ public record UserDto( String userName, int readingGoal, LocalDate goalDate, - int readBooks + int readBooks, + String gitHubId, + String role ) { } diff --git a/backend/src/main/java/com/github/esgoet/backend/user/models/User.java b/backend/src/main/java/com/github/esgoet/backend/user/models/User.java index 5a1d7b0..6da01cd 100644 --- a/backend/src/main/java/com/github/esgoet/backend/user/models/User.java +++ b/backend/src/main/java/com/github/esgoet/backend/user/models/User.java @@ -12,7 +12,9 @@ public record User ( String userName, int readingGoal, LocalDate goalDate, - int readBooks + int readBooks, + String gitHubId, + String role ) { } diff --git a/backend/src/main/java/com/github/esgoet/backend/user/repositories/UserRepository.java b/backend/src/main/java/com/github/esgoet/backend/user/repositories/UserRepository.java index 1e1b3eb..bb4e28a 100644 --- a/backend/src/main/java/com/github/esgoet/backend/user/repositories/UserRepository.java +++ b/backend/src/main/java/com/github/esgoet/backend/user/repositories/UserRepository.java @@ -6,4 +6,5 @@ @Repository public interface UserRepository extends MongoRepository { + User findByGitHubId(String gitHubId); } diff --git a/backend/src/main/java/com/github/esgoet/backend/user/services/UserService.java b/backend/src/main/java/com/github/esgoet/backend/user/services/UserService.java index b19b000..6582d5b 100644 --- a/backend/src/main/java/com/github/esgoet/backend/user/services/UserService.java +++ b/backend/src/main/java/com/github/esgoet/backend/user/services/UserService.java @@ -27,7 +27,7 @@ public User getUserById(String id) { } public User saveUser(UserDto userDto) { - User userToSave = new User(idService.randomId(), userDto.userName(), userDto.readingGoal(), userDto.goalDate(), userDto.readBooks()); + User userToSave = new User(idService.randomId(), userDto.userName(), userDto.readingGoal(), userDto.goalDate(), userDto.readBooks(), userDto.gitHubId(), userDto.role()); return userRepository.save(userToSave); } @@ -44,4 +44,8 @@ public User updateUser(String id, UserDto updatedUser) { public void deleteUser(String id) { userRepository.deleteById(id); } + + public User getUserByGitHubId(String gitHubId) { + return userRepository.findByGitHubId(gitHubId); + } } diff --git a/backend/src/test/java/com/github/esgoet/backend/security/AuthControllerTest.java b/backend/src/test/java/com/github/esgoet/backend/security/AuthControllerTest.java index 50edf88..c17f3ba 100644 --- a/backend/src/test/java/com/github/esgoet/backend/security/AuthControllerTest.java +++ b/backend/src/test/java/com/github/esgoet/backend/security/AuthControllerTest.java @@ -27,6 +27,4 @@ void getLoggedInUserTest() throws Exception { .andExpect(status().isOk()) .andExpect(content().string("esgoet")); } - - } From 8e190b4496f3344f01a49979726fb6308ef1bfc3 Mon Sep 17 00:00:00 2001 From: esgoet Date: Fri, 23 Aug 2024 16:51:05 +0200 Subject: [PATCH 6/8] Add login/logout button to header --- frontend/src/App.tsx | 44 ++++++++----------- frontend/src/components/header/Header.css | 10 +++++ frontend/src/components/header/Header.tsx | 15 ++++++- .../src/components/navigation/Navigation.tsx | 3 -- .../DashboardPage/dashboard/Dashboard.css | 12 ++--- .../DashboardPage/dashboard/Dashboard.tsx | 6 +-- 6 files changed, 51 insertions(+), 39 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6efdf02..ed5db73 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -15,7 +15,7 @@ import LoginPage from "./pages/LoginPage/loginPage/LoginPage.tsx"; export default function App() { const [data, setData] = useState([]) - const [user, setUser] = useState({id: "", userName: "", readingGoal: 0, goalDate: "", readBooks: 0}) + const [user, setUser] = useState() const fetchBooks = () => { axios.get("/api/books") @@ -27,14 +27,6 @@ export default function App() { }) } - const fetchUser = () => { - axios.get("/api/users/1") - .then((response) => { - setUser(response.data) - }) - .catch((error) => (console.log(error))) - } - const deleteBook = (id: string) => { axios.delete("/api/books/" + id) .then((response) => response.status === 200 && fetchBooks()) @@ -48,13 +40,19 @@ export default function App() { } const updateUser = (updatedProperty: string, updatedValue: number | string) => { - axios.put(`/api/users/${user.id}`, {...user, [updatedProperty]: updatedValue}) - .then((response) => response.status === 200 && fetchUser()) + axios.put(`/api/users/${user?.id}`, {...user, [updatedProperty]: updatedValue}) + .then((response) => response.status === 200 && loadUser()) + } + const loadUser = () => { + axios.get("/api/auth/me") + .then((response) => setUser(response.data)) + .catch(() => setUser(null)) } + useEffect(() => { fetchBooks() - fetchUser() + loadUser() }, []); const [searchInput, setSearchInput] = useState("") @@ -72,30 +70,24 @@ export default function App() { window.open(host + "/logout", "_self") } - const loadUser = () => { - axios.get("/api/auth/me") - .then((response) => console.log(response.data)) - .catch((error) => console.log(error)) - } return ( <> -
    - - - +
    + {user && }
    }/> }/> }/> - }/> - }/>} + {user && }/> - }/> + updateUser={updateUser}/>}/>} + {user && }/>}
    diff --git a/frontend/src/components/header/Header.css b/frontend/src/components/header/Header.css index 4edbcea..2171615 100644 --- a/frontend/src/components/header/Header.css +++ b/frontend/src/components/header/Header.css @@ -2,6 +2,8 @@ header { background-color: #183A37; width: 100%; padding: 15px; + display: flex; + justify-content: space-between; } header div { @@ -14,6 +16,14 @@ header div { justify-content: start; } + +.profile a, .profile button { + display: flex; + align-content: center; + place-items: center; + gap: 3px; +} + #logo { width: 50px; grid-area: logo; diff --git a/frontend/src/components/header/Header.tsx b/frontend/src/components/header/Header.tsx index 0fe957b..5735070 100644 --- a/frontend/src/components/header/Header.tsx +++ b/frontend/src/components/header/Header.tsx @@ -1,6 +1,12 @@ import "./Header.css"; +import {Link} from "react-router-dom"; +import {User} from "../../types/types.ts"; -export default function Header() { +type HeaderProps = { + user: User | null | undefined, + logout: () => void +} +export default function Header({user, logout}: HeaderProps) { return (
    @@ -8,6 +14,13 @@ export default function Header() {

    TaleTrail

    Discover, Track, Repeat ( ͡° ͜ʖ ͡°)

    +
    + {user ? : user === null && +login +Login} +
    ) } \ No newline at end of file diff --git a/frontend/src/components/navigation/Navigation.tsx b/frontend/src/components/navigation/Navigation.tsx index 8ba1e87..3b18d64 100644 --- a/frontend/src/components/navigation/Navigation.tsx +++ b/frontend/src/components/navigation/Navigation.tsx @@ -17,9 +17,6 @@ export default function Navigation() {
  • settingsSettings
  • -
  • -login -Login
  • ) diff --git a/frontend/src/pages/DashboardPage/dashboard/Dashboard.css b/frontend/src/pages/DashboardPage/dashboard/Dashboard.css index 6373e83..52da282 100644 --- a/frontend/src/pages/DashboardPage/dashboard/Dashboard.css +++ b/frontend/src/pages/DashboardPage/dashboard/Dashboard.css @@ -1,15 +1,15 @@ +#dashboard-page { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} @media only screen and (max-width: 768px) { .dashboard-goal-summary { justify-content: center; } - .dashboard-page { - display: flex; - align-items: center; - justify-content: center; - } - .dashboard-recent-books { width: 350px; margin: auto; diff --git a/frontend/src/pages/DashboardPage/dashboard/Dashboard.tsx b/frontend/src/pages/DashboardPage/dashboard/Dashboard.tsx index b2bd062..679e28c 100644 --- a/frontend/src/pages/DashboardPage/dashboard/Dashboard.tsx +++ b/frontend/src/pages/DashboardPage/dashboard/Dashboard.tsx @@ -4,7 +4,7 @@ import {Book, User} from "../../../types/types.ts"; import LastAddedBook from "../components/lastAddedBook/LastAddedBook.tsx"; type DashboardProps = { - user: User, + user: User | null | undefined, data: Book[] } @@ -12,9 +12,9 @@ export default function Dashboard({user, data}: DashboardProps) { return (
    -

    Welcome to TaleTrail, {user.userName}!

    +

    Welcome to TaleTrail{user && `, ${user.userName}!`}

    - + {user && }
    From ca0fb2aca6bb398a52db9fbf3c8b9f1bbd779aaa Mon Sep 17 00:00:00 2001 From: esgoet Date: Fri, 23 Aug 2024 17:00:14 +0200 Subject: [PATCH 7/8] Fix double user creation in login --- .../com/github/esgoet/backend/security/SecurityConfig.java | 2 +- .../esgoet/backend/user/repositories/UserRepository.java | 4 +++- .../com/github/esgoet/backend/user/services/UserService.java | 3 +-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/com/github/esgoet/backend/security/SecurityConfig.java b/backend/src/main/java/com/github/esgoet/backend/security/SecurityConfig.java index 6388087..4a436b5 100644 --- a/backend/src/main/java/com/github/esgoet/backend/security/SecurityConfig.java +++ b/backend/src/main/java/com/github/esgoet/backend/security/SecurityConfig.java @@ -59,7 +59,7 @@ public OAuth2UserService oauth2UserService() { OAuth2User user = delegate.loadUser(request); User githubUser; try { - githubUser = userService.getUserById(user.getName()); + githubUser = userService.getUserByGitHubId(user.getName()); } catch (UserNotFoundException e) { githubUser = userService.saveUser(new UserDto(user.getAttributes().get("login").toString(), 0, diff --git a/backend/src/main/java/com/github/esgoet/backend/user/repositories/UserRepository.java b/backend/src/main/java/com/github/esgoet/backend/user/repositories/UserRepository.java index bb4e28a..ae4891e 100644 --- a/backend/src/main/java/com/github/esgoet/backend/user/repositories/UserRepository.java +++ b/backend/src/main/java/com/github/esgoet/backend/user/repositories/UserRepository.java @@ -4,7 +4,9 @@ import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface UserRepository extends MongoRepository { - User findByGitHubId(String gitHubId); + Optional findByGitHubId(String gitHubId); } diff --git a/backend/src/main/java/com/github/esgoet/backend/user/services/UserService.java b/backend/src/main/java/com/github/esgoet/backend/user/services/UserService.java index 6582d5b..76fd458 100644 --- a/backend/src/main/java/com/github/esgoet/backend/user/services/UserService.java +++ b/backend/src/main/java/com/github/esgoet/backend/user/services/UserService.java @@ -37,7 +37,6 @@ public User updateUser(String id, UserDto updatedUser) { .withGoalDate(updatedUser.goalDate()) .withReadBooks(updatedUser.readBooks()) .withReadingGoal(updatedUser.readingGoal()); - return userRepository.save(user); } @@ -46,6 +45,6 @@ public void deleteUser(String id) { } public User getUserByGitHubId(String gitHubId) { - return userRepository.findByGitHubId(gitHubId); + return userRepository.findByGitHubId(gitHubId).orElseThrow(() -> new UserNotFoundException("No user found with GitHub id: " + gitHubId)); } } From 03f22a14ac15bf312001a7784c276be1adb991c9 Mon Sep 17 00:00:00 2001 From: esgoet Date: Fri, 23 Aug 2024 17:14:34 +0200 Subject: [PATCH 8/8] Add githubId and role to test --- .../backend/security/AuthControllerTest.java | 23 ++++++++++++-- .../UserControllerIntegrationTest.java | 26 +++++++++++----- .../user/services/UserServiceTest.java | 30 +++++++++---------- 3 files changed, 53 insertions(+), 26 deletions(-) diff --git a/backend/src/test/java/com/github/esgoet/backend/security/AuthControllerTest.java b/backend/src/test/java/com/github/esgoet/backend/security/AuthControllerTest.java index c17f3ba..0e07555 100644 --- a/backend/src/test/java/com/github/esgoet/backend/security/AuthControllerTest.java +++ b/backend/src/test/java/com/github/esgoet/backend/security/AuthControllerTest.java @@ -1,5 +1,7 @@ package com.github.esgoet.backend.security; +import com.github.esgoet.backend.user.models.User; +import com.github.esgoet.backend.user.repositories.UserRepository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -7,24 +9,39 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.web.servlet.MockMvc; +import java.time.LocalDate; + import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc class AuthControllerTest { @Autowired MockMvc mockMvc; + @Autowired + UserRepository userRepository; @Test @DirtiesContext void getLoggedInUserTest() throws Exception { + userRepository.save(new User("1","esgoet", 0, LocalDate.now(), 0, "123", "USER")); + mockMvc.perform(get("/api/auth/me") .with(oidcLogin().idToken(token -> token.subject("123")) .userInfoToken(token -> token.claim("login", "esgoet")))) .andExpect(status().isOk()) - .andExpect(content().string("esgoet")); + .andExpect(content().json(""" + { + "userName": "esgoet", + "readingGoal": 0, + "readBooks": 0, + "gitHubId": "123", + "role": "USER" + } + """)) + .andExpect(jsonPath("$.id").exists()) + .andExpect(jsonPath("$.goalDate").exists()); } } diff --git a/backend/src/test/java/com/github/esgoet/backend/user/controllers/UserControllerIntegrationTest.java b/backend/src/test/java/com/github/esgoet/backend/user/controllers/UserControllerIntegrationTest.java index e0ec3e6..f88d1a7 100644 --- a/backend/src/test/java/com/github/esgoet/backend/user/controllers/UserControllerIntegrationTest.java +++ b/backend/src/test/java/com/github/esgoet/backend/user/controllers/UserControllerIntegrationTest.java @@ -39,7 +39,7 @@ void getUsersTest() throws Exception { @WithMockUser void getUserByIdTest_whenIdExists() throws Exception{ //GIVEN - userRepository.save(new User("1","esgoet", 6, goalDate, 0)); + userRepository.save(new User("1","esgoet", 6, goalDate, 0, "123", "USER")); //WHEN mockMvc.perform(get("/api/users/1")) //THEN @@ -50,7 +50,9 @@ void getUserByIdTest_whenIdExists() throws Exception{ "userName": "esgoet", "readingGoal": 6, "goalDate": "2024-12-31", - "readBooks": 0 + "readBooks": 0, + "gitHubId": "123", + "role": "USER" } """)); } @@ -83,7 +85,9 @@ void addUserTest() throws Exception { "userName": "esgoet", "readingGoal": 6, "goalDate": "2024-12-31", - "readBooks": 0 + "readBooks": 0, + "gitHubId": "123", + "role": "USER" } """)) .andExpect(status().isOk()) @@ -92,7 +96,9 @@ void addUserTest() throws Exception { "userName": "esgoet", "readingGoal": 6, "goalDate": "2024-12-31", - "readBooks": 0 + "readBooks": 0, + "gitHubId": "123", + "role": "USER" } """)) .andExpect(jsonPath("$.id").exists()); @@ -103,7 +109,7 @@ void addUserTest() throws Exception { @WithMockUser void updateUserTest() throws Exception { //GIVEN - userRepository.save(new User("1","esgoet", 6, goalDate, 0)); + userRepository.save(new User("1","esgoet", 6, goalDate, 0, "123", "USER")); //WHEN mockMvc.perform(put("/api/users/1") @@ -113,7 +119,9 @@ void updateUserTest() throws Exception { "userName": "esgoet", "readingGoal": 6, "goalDate": "2024-12-31", - "readBooks": 1 + "readBooks": 1, + "gitHubId": "123", + "role": "USER" } """)) //THEN @@ -124,7 +132,9 @@ void updateUserTest() throws Exception { "userName": "esgoet", "readingGoal": 6, "goalDate": "2024-12-31", - "readBooks": 1 + "readBooks": 1, + "gitHubId": "123", + "role": "USER" } """)); } @@ -134,7 +144,7 @@ void updateUserTest() throws Exception { @WithMockUser void deleteUserTest() throws Exception { //GIVEN - userRepository.save(new User("1","esgoet", 6, goalDate, 0)); + userRepository.save(new User("1","esgoet", 6, goalDate, 0, "123", "USER")); //WHEN mockMvc.perform(delete("/api/users/1")) diff --git a/backend/src/test/java/com/github/esgoet/backend/user/services/UserServiceTest.java b/backend/src/test/java/com/github/esgoet/backend/user/services/UserServiceTest.java index 58c7f98..4325691 100644 --- a/backend/src/test/java/com/github/esgoet/backend/user/services/UserServiceTest.java +++ b/backend/src/test/java/com/github/esgoet/backend/user/services/UserServiceTest.java @@ -25,13 +25,13 @@ class UserServiceTest { @Test void getUsers_Test() { List users = List.of( - new User("1","user1", 6, goalDate, 0), - new User("2","user2", 22, goalDate, 5) + new User("1","user1", 6, goalDate, 0,"123", "USER"), + new User("2","user2", 22, goalDate, 5, "123", "USER") ); List expectedUsers = List.of( - new User("1","user1", 6, goalDate, 0), - new User("2","user2", 22, goalDate, 5) + new User("1","user1", 6, goalDate, 0, "123", "USER"), + new User("2","user2", 22, goalDate, 5, "123", "USER") ); when(userRepo.findAll()).thenReturn(users); @@ -52,12 +52,12 @@ void getUsersTest_whenEmpty_thenReturnEmptyList() { @Test void getUserByIdTest_whenUserExists_thenReturnUser() { //GIVEN - User user = new User("1","user1", 6, goalDate, 0); + User user = new User("1","user1", 6, goalDate, 0, "123", "USER"); when(userRepo.findById("1")).thenReturn(Optional.of(user)); //WHEN User actual = userService.getUserById("1"); //THEN - User expected = new User("1","user1", 6, goalDate, 0); + User expected = new User("1","user1", 6, goalDate, 0, "123", "USER"); verify(userRepo).findById("1"); assertEquals(expected, actual); } @@ -75,8 +75,8 @@ void getUserByIdTest_whenUserDoesNotExists_thenThrow() { @Test void addUserTest_whenNewUserAsInput_thenReturnNewUser() { // GIVEN - UserDto userDto = new UserDto("user1", 6, goalDate, 0); - User userToSave = new User("1","user1", 6, goalDate, 0); + UserDto userDto = new UserDto("user1", 6, goalDate, 0, "123", "USER"); + User userToSave = new User("1","user1", 6, goalDate, 0, "123", "USER"); when(idService.randomId()).thenReturn("1"); when(userRepo.save(userToSave)).thenReturn(userToSave); @@ -84,7 +84,7 @@ void addUserTest_whenNewUserAsInput_thenReturnNewUser() { User actual = userService.saveUser(userDto); // THEN - User expected = new User("1","user1", 6, goalDate, 0); + User expected = new User("1","user1", 6, goalDate, 0, "123", "USER"); verify(idService).randomId(); verify(userRepo).save(userToSave); assertEquals(expected, actual); @@ -102,9 +102,9 @@ void updateUserTest_whenUserExists() { // Given String id = "1"; - User existingUser = new User("1","user1", 6, goalDate, 0); - UserDto updatedUserDto = new UserDto("user1", 6, goalDate, 1); - User updatedUser = new User("1","user1", 6, goalDate, 1); + User existingUser = new User("1","user1", 6, goalDate, 0, "123", "USER"); + UserDto updatedUserDto = new UserDto("user1", 6, goalDate, 1, "123", "USER"); + User updatedUser = new User("1","user1", 6, goalDate, 1, "123", "USER"); // When when(userRepo.findById(id)).thenReturn(Optional.of(existingUser)); @@ -113,7 +113,7 @@ void updateUserTest_whenUserExists() { User actual = userService.updateUser(id, updatedUserDto); // Then - User expected = new User("1","user1", 6, goalDate, 1); + User expected = new User("1","user1", 6, goalDate, 1,"123", "USER"); assertNotNull(actual); assertEquals(expected, actual); verify(userRepo).findById(id); @@ -125,8 +125,8 @@ void updateUserTest_whenUserNotFound() { // Given String id = "1"; - UserDto updatedUserDto = new UserDto("user1", 6, goalDate, 1); - User updatedUser = new User("1","user1", 6, goalDate, 1); + UserDto updatedUserDto = new UserDto("user1", 6, goalDate, 1,"123", "USER"); + User updatedUser = new User("1","user1", 6, goalDate, 1,"123", "USER"); //When when(userRepo.findById(id)).thenReturn(Optional.empty());