diff --git a/backend/pom.xml b/backend/pom.xml index 43eeb21..d233957 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -29,10 +29,19 @@ 4.9.3 test + + org.springframework.security + spring-security-test + test + org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-oauth2-client + org.projectlombok diff --git a/backend/src/main/java/com/github/iskrendev/insuranceprogram/controllers/AuthenticationController.java b/backend/src/main/java/com/github/iskrendev/insuranceprogram/controllers/AuthenticationController.java new file mode 100644 index 0000000..efb26f6 --- /dev/null +++ b/backend/src/main/java/com/github/iskrendev/insuranceprogram/controllers/AuthenticationController.java @@ -0,0 +1,25 @@ +package com.github.iskrendev.insuranceprogram.controllers; + +import com.github.iskrendev.insuranceprogram.models.AppUser; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/auth") +public class AuthenticationController { + + @GetMapping("/me") + public AppUser getMe() { + var principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + if (principal instanceof DefaultOAuth2User defaultOAuth2User) { + return AppUser.builder() + .id(Integer.parseInt(defaultOAuth2User.getAttributes().get("id").toString())) + .login(defaultOAuth2User.getAttributes().get("login").toString()) + .build(); + } + throw new IllegalArgumentException("No user logged in"); + } +} diff --git a/backend/src/main/java/com/github/iskrendev/insuranceprogram/models/AppUser.java b/backend/src/main/java/com/github/iskrendev/insuranceprogram/models/AppUser.java new file mode 100644 index 0000000..80e8746 --- /dev/null +++ b/backend/src/main/java/com/github/iskrendev/insuranceprogram/models/AppUser.java @@ -0,0 +1,14 @@ +package com.github.iskrendev.insuranceprogram.models; + +import lombok.Builder; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +@Builder +@Document(collection = "users") +public record AppUser( + @Id + int id, + String login +) { +} diff --git a/backend/src/main/java/com/github/iskrendev/insuranceprogram/security/SecurityConfig.java b/backend/src/main/java/com/github/iskrendev/insuranceprogram/security/SecurityConfig.java new file mode 100644 index 0000000..ae12db2 --- /dev/null +++ b/backend/src/main/java/com/github/iskrendev/insuranceprogram/security/SecurityConfig.java @@ -0,0 +1,53 @@ +package com.github.iskrendev.insuranceprogram.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("local") + private String environment; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(a -> a + .requestMatchers("/api/**").authenticated() + .anyRequest().permitAll() + ) + .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)) + .exceptionHandling(exceptionHandlingConfigurer -> + exceptionHandlingConfigurer.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))) + .logout(l -> { + if (environment.equals("prod")) { + l.logoutSuccessUrl("/").permitAll(); + } else { + l.logoutSuccessUrl("http://localhost:5173").permitAll(); + } + }) + .oauth2Login(o -> { + try { + o.init(http); + if (environment.equals("prod")) { + o.defaultSuccessUrl("/home", true); + } else { + o.defaultSuccessUrl("http://localhost:5173/home", true); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + return http.build(); + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 3250028..a9d7664 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1,2 +1,6 @@ spring.data.mongodb.uri=${MONGODB_URI} -spring.mvc.hiddenmethod.filter.enabled= true \ No newline at end of file +spring.mvc.hiddenmethod.filter.enabled= true +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 +myapp.environment=local \ No newline at end of file diff --git a/backend/src/test/java/com/github/iskrendev/insuranceprogram/controllers/AllInsurancesControllerTest.java b/backend/src/test/java/com/github/iskrendev/insuranceprogram/controllers/AllInsurancesControllerTest.java index 920bf79..ce558ec 100644 --- a/backend/src/test/java/com/github/iskrendev/insuranceprogram/controllers/AllInsurancesControllerTest.java +++ b/backend/src/test/java/com/github/iskrendev/insuranceprogram/controllers/AllInsurancesControllerTest.java @@ -13,6 +13,7 @@ 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.security.test.context.support.WithMockUser; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.web.servlet.MockMvc; @@ -44,6 +45,7 @@ class AllInsurancesControllerTest { @Test @DirtiesContext + @WithMockUser void getAllInsurances_whenNoInsurancesAreInLists_thenReturnEmptyLists() throws Exception { mockMvc.perform(get(BASE_URI)) .andExpect(jsonPath("$.lifeInsurances", hasSize(0))) @@ -53,6 +55,7 @@ void getAllInsurances_whenNoInsurancesAreInLists_thenReturnEmptyLists() throws E @Test @DirtiesContext + @WithMockUser void getAllInsurances_whenOneInsuranceIsInEachList_thenReturnLists() throws Exception { LifeInsurance lifeInsurance = LifeInsurance.builder() .id("1") diff --git a/backend/src/test/java/com/github/iskrendev/insuranceprogram/controllers/AuthenticationControllerTest.java b/backend/src/test/java/com/github/iskrendev/insuranceprogram/controllers/AuthenticationControllerTest.java new file mode 100644 index 0000000..e78ab9a --- /dev/null +++ b/backend/src/test/java/com/github/iskrendev/insuranceprogram/controllers/AuthenticationControllerTest.java @@ -0,0 +1,50 @@ +package com.github.iskrendev.insuranceprogram.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.iskrendev.insuranceprogram.models.AppUser; +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.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class AuthenticationControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Test + @DirtiesContext + void getMe_whenLoggedIn_expectStatus200() throws Exception { + AppUser expectedAppUser = new AppUser(1, "test"); + String expectedJson = objectMapper.writeValueAsString(expectedAppUser); + mockMvc.perform(get("/api/auth/me") + .with(oidcLogin().userInfoToken(token -> { + token.claims(claim -> { + claim.put("id", "1"); + claim.put("login", "test"); + }); + })) + ) + .andExpect(status().isOk()) + .andExpect(content().json(expectedJson)); + } + + @Test + @DirtiesContext + void getMe_whenNotLoggedIn_expectStatus401() throws Exception { + mockMvc.perform(get("/api/auth/me")) + .andExpect(status().isUnauthorized()); + } +} \ No newline at end of file diff --git a/backend/src/test/java/com/github/iskrendev/insuranceprogram/controllers/LifeInsuranceControllerTest.java b/backend/src/test/java/com/github/iskrendev/insuranceprogram/controllers/LifeInsuranceControllerTest.java index f683f28..b2a47b9 100644 --- a/backend/src/test/java/com/github/iskrendev/insuranceprogram/controllers/LifeInsuranceControllerTest.java +++ b/backend/src/test/java/com/github/iskrendev/insuranceprogram/controllers/LifeInsuranceControllerTest.java @@ -11,6 +11,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; @@ -34,6 +35,7 @@ class LifeInsuranceControllerTest { @Test @DirtiesContext + @WithMockUser void getAllLifeInsurances_whenNoLifeInsuranceIsInList_thenReturnEmptyList() throws Exception { mockMvc.perform(get(BASE_URI)) .andExpect(status().isOk()) @@ -42,6 +44,7 @@ void getAllLifeInsurances_whenNoLifeInsuranceIsInList_thenReturnEmptyList() thro @Test @DirtiesContext + @WithMockUser void getAllLifeInsurances_whenOneLifeInsuranceIsInList_thenReturnList() throws Exception { LifeInsurance lifeInsurance = LifeInsurance.builder() .id("1") @@ -72,6 +75,7 @@ void getAllLifeInsurances_whenOneLifeInsuranceIsInList_thenReturnList() throws E @Test @DirtiesContext + @WithMockUser void getLifeInsuranceById_whenIdIsValid_thenReturnInsurance() throws Exception { LifeInsurance lifeInsurance = LifeInsurance.builder() .id("1") @@ -100,6 +104,7 @@ void getLifeInsuranceById_whenIdIsValid_thenReturnInsurance() throws Exception { @Test @DirtiesContext + @WithMockUser void getLifeInsuranceById_whenIdIsNotValid_thenThrowException() throws Exception { mockMvc.perform(get(BASE_URI + "/invalidId")) .andExpect(status().isNotFound()) @@ -108,6 +113,7 @@ void getLifeInsuranceById_whenIdIsNotValid_thenThrowException() throws Exception @Test @DirtiesContext + @WithMockUser void addLifeInsurance_whenDataIsComplete_thenReturnCompleteInsurance() throws Exception { LifeInsuranceDTO newLifeInsurance = LifeInsuranceDTO.builder() .firstName("TestFirstName") @@ -136,6 +142,7 @@ void addLifeInsurance_whenDataIsComplete_thenReturnCompleteInsurance() throws Ex @Test @DirtiesContext + @WithMockUser void addLifeInsurance_whenJustOneFieldIsFilledOut_thenReturnNullForEmptyFields() throws Exception { LifeInsuranceDTO newLifeInsurance = LifeInsuranceDTO.builder() .firstName("TestFirstName") @@ -163,6 +170,7 @@ void addLifeInsurance_whenJustOneFieldIsFilledOut_thenReturnNullForEmptyFields() @Test @DirtiesContext + @WithMockUser void updateLifeInsurance_whenInsuranceIdExistsInDb_thenReturnUpdatedInsurance() throws Exception { LifeInsurance lifeInsuranceBefore = LifeInsurance.builder() .id("1") @@ -233,6 +241,7 @@ void updateLifeInsurance_whenInsuranceIdExistsInDb_thenReturnUpdatedInsurance() @Test @DirtiesContext + @WithMockUser void updateLifeInsurance_whenInsuranceIdDoesNotExistsInDb_thenThrowException() throws Exception { LifeInsuranceUpdateDTO lifeInsuranceUpdateDTO = LifeInsuranceUpdateDTO.builder() .firstName("TestFirstName") @@ -260,6 +269,7 @@ void updateLifeInsurance_whenInsuranceIdDoesNotExistsInDb_thenThrowException() t @Test @DirtiesContext + @WithMockUser void deleteLifeInsurance() throws Exception { LifeInsurance lifeInsurance = LifeInsurance.builder() .id("1") diff --git a/backend/src/test/java/com/github/iskrendev/insuranceprogram/controllers/PropertyInsuranceControllerTest.java b/backend/src/test/java/com/github/iskrendev/insuranceprogram/controllers/PropertyInsuranceControllerTest.java index a50a35e..d512113 100644 --- a/backend/src/test/java/com/github/iskrendev/insuranceprogram/controllers/PropertyInsuranceControllerTest.java +++ b/backend/src/test/java/com/github/iskrendev/insuranceprogram/controllers/PropertyInsuranceControllerTest.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; @@ -32,6 +33,7 @@ class PropertyInsuranceControllerTest { @Test @DirtiesContext + @WithMockUser void getAllPropertyInsurances_whenNoPropertyInsuranceIsInList_thenReturnEmptyList() throws Exception { mockMvc.perform(get(BASE_URI)) .andExpect(status().isOk()) @@ -40,6 +42,7 @@ void getAllPropertyInsurances_whenNoPropertyInsuranceIsInList_thenReturnEmptyLis @Test @DirtiesContext + @WithMockUser void getAllPropertyInsurances_whenOnePropertyInsuranceIsInList_thenReturnList() throws Exception { PropertyInsurance propertyInsurance = PropertyInsurance.builder() .id("1") @@ -71,6 +74,7 @@ void getAllPropertyInsurances_whenOnePropertyInsuranceIsInList_thenReturnList() @Test @DirtiesContext + @WithMockUser void getPropertyInsuranceById_whenIdIsValid_thenReturnInsurance() throws Exception { PropertyInsurance propertyInsurance = PropertyInsurance.builder() .id("1") @@ -100,6 +104,7 @@ void getPropertyInsuranceById_whenIdIsValid_thenReturnInsurance() throws Excepti @Test @DirtiesContext + @WithMockUser void getPropertyInsuranceById_whenIdIsNotValid_thenThrowException() throws Exception { mockMvc.perform(get(BASE_URI + "/invalidId")) .andExpect(status().isNotFound()) @@ -108,6 +113,7 @@ void getPropertyInsuranceById_whenIdIsNotValid_thenThrowException() throws Excep @Test @DirtiesContext + @WithMockUser void addPropertyInsurance_whenDataIsComplete_thenReturnCompleteInsurance() throws Exception { PropertyInsuranceDTO newPropertyInsurance = PropertyInsuranceDTO.builder() .firstName("TestFirstName") @@ -137,6 +143,7 @@ void addPropertyInsurance_whenDataIsComplete_thenReturnCompleteInsurance() throw @Test @DirtiesContext + @WithMockUser void addPropertyInsurance_whenJustOneFieldIsFilledOut_thenReturnNullForEmptyFields() throws Exception { PropertyInsuranceDTO newPropertyInsurance = PropertyInsuranceDTO.builder() .firstName("TestFirstName") @@ -166,6 +173,7 @@ void addPropertyInsurance_whenJustOneFieldIsFilledOut_thenReturnNullForEmptyFiel @Test @DirtiesContext + @WithMockUser void updatePropertyInsurance_whenInsuranceIdExistsInDb_thenReturnUpdatedInsurance() throws Exception { PropertyInsurance propertyInsuranceBefore = PropertyInsurance.builder() .id("1") @@ -239,6 +247,7 @@ void updatePropertyInsurance_whenInsuranceIdExistsInDb_thenReturnUpdatedInsuranc @Test @DirtiesContext + @WithMockUser void updatePropertyInsurance_whenInsuranceIdDoesNotExistsInDb_thenThrowException() throws Exception { PropertyInsuranceUpdateDTO propertyInsuranceUpdateDTO = PropertyInsuranceUpdateDTO.builder() .firstName("TestFirstName") @@ -267,6 +276,7 @@ void updatePropertyInsurance_whenInsuranceIdDoesNotExistsInDb_thenThrowException @Test @DirtiesContext + @WithMockUser void deletePropertyInsurance() throws Exception { PropertyInsurance propertyInsurance = PropertyInsurance.builder() .id("1") diff --git a/backend/src/test/java/com/github/iskrendev/insuranceprogram/controllers/VehicleInsuranceControllerTest.java b/backend/src/test/java/com/github/iskrendev/insuranceprogram/controllers/VehicleInsuranceControllerTest.java index 2c255bf..440b350 100644 --- a/backend/src/test/java/com/github/iskrendev/insuranceprogram/controllers/VehicleInsuranceControllerTest.java +++ b/backend/src/test/java/com/github/iskrendev/insuranceprogram/controllers/VehicleInsuranceControllerTest.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; @@ -32,6 +33,7 @@ class VehicleInsuranceControllerTest { @Test @DirtiesContext + @WithMockUser void getAllVehicleInsurances_whenNoVehicleInsuranceIsInList_thenReturnEmptyList() throws Exception { mockMvc.perform(get(BASE_URI)) .andExpect(status().isOk()) @@ -40,6 +42,7 @@ void getAllVehicleInsurances_whenNoVehicleInsuranceIsInList_thenReturnEmptyList( @Test @DirtiesContext + @WithMockUser void getAllVehicleInsurances_whenListContainsInsurances_thenReturnListOfInsurances() throws Exception { VehicleInsurance vehicleInsurance = VehicleInsurance.builder() .id("1") @@ -71,6 +74,7 @@ void getAllVehicleInsurances_whenListContainsInsurances_thenReturnListOfInsuranc @Test @DirtiesContext + @WithMockUser void getVehicleInsuranceById_whenIdIsValid_thenReturnInsurance() throws Exception { VehicleInsurance vehicleInsurance = VehicleInsurance.builder() .id("1") @@ -100,6 +104,7 @@ void getVehicleInsuranceById_whenIdIsValid_thenReturnInsurance() throws Exceptio @Test @DirtiesContext + @WithMockUser void getVehicleInsuranceById_whenIdIsNotValid_thenThrowException() throws Exception { mockMvc.perform(get(BASE_URI + "/invalidId")) .andExpect(status().isNotFound()) @@ -108,6 +113,7 @@ void getVehicleInsuranceById_whenIdIsNotValid_thenThrowException() throws Except @Test @DirtiesContext + @WithMockUser void addVehicleInsurance_whenDataIsComplete_thenReturnCompleteInsurance() throws Exception { VehicleInsuranceDTO newVehicleInsurance = VehicleInsuranceDTO.builder() .firstName("TestFirstName") @@ -137,6 +143,7 @@ void addVehicleInsurance_whenDataIsComplete_thenReturnCompleteInsurance() throws @Test @DirtiesContext + @WithMockUser void addVehicleInsurance_whenJustOneFieldIsFilledOut_thenReturnNullForEmptyFields() throws Exception { VehicleInsuranceDTO newVehicleInsurance = VehicleInsuranceDTO.builder() .firstName("TestFirstName") @@ -167,6 +174,7 @@ void addVehicleInsurance_whenJustOneFieldIsFilledOut_thenReturnNullForEmptyField @Test @DirtiesContext + @WithMockUser void updateVehicleInsurance_whenInsuranceIdExistsInDb_thenReturnUpdatedInsurance() throws Exception { VehicleInsurance vehicleInsuranceBefore = VehicleInsurance.builder() .id("1") @@ -243,6 +251,7 @@ void updateVehicleInsurance_whenInsuranceIdExistsInDb_thenReturnUpdatedInsurance @Test @DirtiesContext + @WithMockUser void updateVehicleInsurance_whenInsuranceIdDoesNotExistsInDb_thenThrowException() throws Exception { VehicleInsuranceUpdateDTO vehicleInsuranceUpdateDTO = VehicleInsuranceUpdateDTO.builder() .firstName("TestFirstName") @@ -272,6 +281,7 @@ void updateVehicleInsurance_whenInsuranceIdDoesNotExistsInDb_thenThrowException( @Test @DirtiesContext + @WithMockUser void deleteVehicleInsurance() throws Exception { VehicleInsurance vehicleInsurance = VehicleInsurance.builder() .id("1") diff --git a/backend/src/test/resources/application.properties b/backend/src/test/resources/application.properties index 8b5144d..0c5aff2 100644 --- a/backend/src/test/resources/application.properties +++ b/backend/src/test/resources/application.properties @@ -1 +1,6 @@ -de.flapdoodle.mongodb.embedded.version=6.0.1 \ No newline at end of file +de.flapdoodle.mongodb.embedded.version=6.0.1 +spring.mvc.hiddenmethod.filter.enabled= true +spring.security.oauth2.client.registration.github.client-id="1" +spring.security.oauth2.client.registration.github.client-secret="1" +spring.security.oauth2.client.registration.github.scope=1 +myapp.environment=local \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index e4b78ea..f31ccf9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - Vite + React + TS + Insurance Manager Plus
diff --git a/frontend/src/App.css b/frontend/src/App.css index b9d355d..1f963f4 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -40,3 +40,14 @@ .read-the-docs { color: #888; } + +.login-button { + width: 250px; + height: 40px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #f5f5f5; + box-shadow: 2px 4px 4px rgba(0, 0, 0, 0.1); +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c539004..9698b78 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,24 +1,58 @@ import Header from "./components/header/Header.tsx"; import './App.css' -import {Route, Routes} from "react-router-dom"; -import AddPage from './pages/AddPage.tsx'; +import {useNavigate, Route, Routes} from "react-router-dom"; import HomePage from './pages/HomePage.tsx'; +import AddPage from './pages/AddPage.tsx'; import DetailsPage from "./pages/DetailsPage.tsx"; import EditPage from "./pages/EditPage.tsx"; +import ProtectedRoutes from "./ProtectedRoutes.tsx"; +import {AppUser} from "./types/types.ts"; +import {useEffect, useState} from "react"; +import axios from "axios"; function App() { + const [appUser, setAppUser] = useState(); + + function login() { + const host = window.location.host === 'localhost:5173' ? 'http://localhost:8080' : window.location.origin + window.open(host + '/oauth2/authorization/github', '_self') + } + + const navigate = useNavigate(); + + useEffect(() => { + axios.get("/api/auth/me") + .then((response) => { + setAppUser(response.data); + + if (response.data) { + navigate('/home'); + } + }) + .catch((e) => console.log(e)) + }, []); + + if (!appUser) { + return ( +
+ +
+ ); + } return ( <> -
+
- } /> - } /> - } /> - } /> + }> + }/> + }/> + }/> + }/> + ) } -export default App \ No newline at end of file +export default App; \ No newline at end of file diff --git a/frontend/src/ProtectedRoutes.tsx b/frontend/src/ProtectedRoutes.tsx new file mode 100644 index 0000000..08bf5d9 --- /dev/null +++ b/frontend/src/ProtectedRoutes.tsx @@ -0,0 +1,10 @@ +import {ProtectedRoutesProps} from "./types/types.ts"; +import {Navigate, Outlet} from "react-router-dom"; + +function ProtectedRoutes(props: Readonly) { + const isAuth = props.appUser !== null && props.appUser !== undefined; + + return isAuth ? : ; +} + +export default ProtectedRoutes; \ No newline at end of file diff --git a/frontend/src/components/content/InsuranceList.tsx b/frontend/src/components/content/InsuranceList.tsx index 52411a6..e1ba10f 100644 --- a/frontend/src/components/content/InsuranceList.tsx +++ b/frontend/src/components/content/InsuranceList.tsx @@ -1,40 +1,48 @@ import {InsuranceListProps} from "../../types/types.ts"; import {Link} from "react-router-dom"; import {useState} from "react"; +import "../../pages/SharedComponents.css" function InsuranceList(props: Readonly) { const [sortedInsurances, setSortedInsurances] = useState([...props.insurances]); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); const [isAccordionOpen, setIsAccordionOpen] = useState(false); + const [isListRendered, setIsListRendered] = useState(false); const handleAccordionToggle = () => { setIsAccordionOpen(!isAccordionOpen); + setIsListRendered(true); }; const handleSortToggle = () => { - const newSortOrder = sortOrder === "asc" ? "desc" : "asc"; - setSortOrder(newSortOrder); + if (isListRendered) { + const newSortOrder = sortOrder === "asc" ? "desc" : "asc"; + setSortOrder(newSortOrder); - const sorted = [...props.insurances].sort((a, b) => { - const nameA = `${a.firstName} ${a.familyName}`.toUpperCase(); - const nameB = `${b.firstName} ${b.familyName}`.toUpperCase(); + const sorted = [...props.insurances].sort((a, b) => { + const nameA = `${a.firstName} ${a.familyName}`.toUpperCase(); + const nameB = `${b.firstName} ${b.familyName}`.toUpperCase(); - return newSortOrder === "asc" ? nameA.localeCompare(nameB) : nameB.localeCompare(nameA); - }); + return newSortOrder === "asc" ? nameA.localeCompare(nameB) : nameB.localeCompare(nameA); + }); - setSortedInsurances(sorted); + setSortedInsurances(sorted); + } }; return (
+

{props.headerText}

- - -

{props.headerText}

+ + {isListRendered && ( + + )}
{isAccordionOpen && (
    diff --git a/frontend/src/components/header/Header.css b/frontend/src/components/header/Header.css index ab28844..bce2d77 100644 --- a/frontend/src/components/header/Header.css +++ b/frontend/src/components/header/Header.css @@ -9,7 +9,36 @@ } .nav-main { + display: flex; + align-items: center; padding: 8px; text-align: left; margin-left: 30px; + margin-right: 30px; +} + +.home-icon { + margin-right: auto; +} + +.statistics-icon { + margin-right: 25px; +} + +.button-logout-icon { + background: none; + border: none; + padding: 0; + cursor: pointer; + outline: none; +} + +.button-logout-icon:hover, +.button-logout-icon:focus { + outline: none; + border: none; +} + +.spacer { + flex-grow: 1; } \ No newline at end of file diff --git a/frontend/src/components/header/Header.tsx b/frontend/src/components/header/Header.tsx index fb33b27..6ffad6a 100644 --- a/frontend/src/components/header/Header.tsx +++ b/frontend/src/components/header/Header.tsx @@ -1,17 +1,39 @@ import "./Header.css"; import HomeIcon from "../svg/HomeIcon.tsx"; -import { Link } from "react-router-dom"; +import LogoutIcon from "../svg/LogoutIcon.tsx"; +import { Link, useLocation } from "react-router-dom"; +import StatisticsIcon from "../svg/StatisticsIcon.tsx"; function Header() { - return ( -
    - -
    - ); + const location = useLocation(); + const isLoginPage = location.pathname === '/login'; + const logout = () => { + const host = window.location.host === 'localhost:5173' ? 'http://localhost:8080' : window.location.origin + window.open(host + '/logout', '_self'); + }; + + if (!isLoginPage) { + return ( +
    + +
    + ); + } else { + return ( +
    +
    + ); + } } export default Header; \ No newline at end of file diff --git a/frontend/src/components/svg/LogoutIcon.tsx b/frontend/src/components/svg/LogoutIcon.tsx new file mode 100644 index 0000000..39e3f1b --- /dev/null +++ b/frontend/src/components/svg/LogoutIcon.tsx @@ -0,0 +1,15 @@ +function LogoutIcon() { + return ( + + + + + + + + + + ); +} + +export default LogoutIcon; \ No newline at end of file diff --git a/frontend/src/components/svg/StatisticsIcon.tsx b/frontend/src/components/svg/StatisticsIcon.tsx new file mode 100644 index 0000000..ea7f5f5 --- /dev/null +++ b/frontend/src/components/svg/StatisticsIcon.tsx @@ -0,0 +1,14 @@ +function StatisticsIcon() { + return ( + + + + + + + + + ); +} + +export default StatisticsIcon; \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index fd880ee..b587848 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -26,7 +26,6 @@ body { margin: 0; padding: 0; display: flex; - place-items: center; min-width: 320px; min-height: 100vh; border: 30px solid; @@ -54,7 +53,11 @@ button { background-color: #1a1a1a; cursor: pointer; transition: border-color 0.25s; + display: flex; + align-items: center; + justify-content: center; } + button:hover { border-color: #ffffff; } diff --git a/frontend/src/modals/DeleteConfirmationModal.css b/frontend/src/modals/DeleteConfirmationModal.css index b42ce43..55d0a0b 100644 --- a/frontend/src/modals/DeleteConfirmationModal.css +++ b/frontend/src/modals/DeleteConfirmationModal.css @@ -24,18 +24,13 @@ display: flex; align-items: center; justify-content: center; -} - -.centered-content { - display: flex; - align-items: center; - margin-left: -10px; + margin-right: 16px; } .modal-title { font-size: 1.7em; font-weight: bold; - margin-bottom: 10px; + margin-bottom: 3px; text-align: center; } diff --git a/frontend/src/modals/DeleteConfirmationModal.tsx b/frontend/src/modals/DeleteConfirmationModal.tsx index 8e8f354..c5bf833 100644 --- a/frontend/src/modals/DeleteConfirmationModal.tsx +++ b/frontend/src/modals/DeleteConfirmationModal.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import '../modals/DeleteConfirmationModal.css'; import WarningIcon from '../components/svg/WarningIcon.tsx'; interface DeleteConfirmationModalProps { @@ -8,7 +9,7 @@ interface DeleteConfirmationModalProps { } const DeleteConfirmationModal = (props: DeleteConfirmationModalProps) => { - const { show, handleClose, handleConfirm } = props; + const {show, handleClose, handleConfirm} = props; const handleCloseKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter' || event.key === ' ') { @@ -27,10 +28,8 @@ const DeleteConfirmationModal = (props: DeleteConfirmationModalProps) => {
    -
    - -
    Bestätigung
    -
    + +
    Bestätigung

    Sind Sie sicher, dass Sie diesen Versicherungseintrag löschen möchten?

    diff --git a/frontend/src/pages/AddPage.tsx b/frontend/src/pages/AddPage.tsx index 11e04af..5f851b6 100644 --- a/frontend/src/pages/AddPage.tsx +++ b/frontend/src/pages/AddPage.tsx @@ -1,11 +1,10 @@ -import "./AddPage.css"; +import "./SharedComponents.css"; import React, {useState} from 'react'; import axios, {AxiosError} from "axios"; import {useNavigate} from "react-router-dom"; import FormLabel from "../components/content/FormLabel.tsx"; import moment from "moment"; - function AddPage() { const [firstName, setFirstName] = useState(""); const [familyName, setFamilyName] = useState(""); @@ -75,7 +74,7 @@ function AddPage() { axios.post(saveEndpoint, newInsuranceData) .then(() => { - navigate('/') + navigate('/home') }) .catch((error: AxiosError) => { console.error('Error adding data:', error); @@ -103,7 +102,7 @@ function AddPage() { handleOnChangeText={setCity}/> - diff --git a/frontend/src/pages/DetailsPage.tsx b/frontend/src/pages/DetailsPage.tsx index 2833588..94f7b58 100644 --- a/frontend/src/pages/DetailsPage.tsx +++ b/frontend/src/pages/DetailsPage.tsx @@ -1,5 +1,5 @@ -import "./AddPage.css"; import "./DetailsPage.css"; +import "./SharedComponents.css"; import {Link, useParams} from "react-router-dom"; import axios, {AxiosResponse} from "axios"; import {useEffect, useState} from "react"; diff --git a/frontend/src/pages/EditPage.tsx b/frontend/src/pages/EditPage.tsx index 369a55b..2cb7ddc 100644 --- a/frontend/src/pages/EditPage.tsx +++ b/frontend/src/pages/EditPage.tsx @@ -1,5 +1,5 @@ import "./EditPage.css"; -import '../modals/DeleteConfirmationModal.css'; +import "./SharedComponents.css"; import {useEffect, useState} from 'react'; import axios, {AxiosError, AxiosResponse} from "axios"; import {NavigateFunction, useNavigate, useParams} from "react-router-dom"; @@ -58,7 +58,7 @@ function EditPage() { const editedInsuranceData = {...insuranceData, type}; axios .put(`/api/${type}/${id}`, editedInsuranceData) - .then(() => navigate("/")) + .then(() => navigate("/home")) .catch(error => { console.error('Error updating insurance data:', error); }); @@ -81,7 +81,7 @@ function EditPage() { axios .delete(`/api/${type}/${id}`) .then(() => { - navigate("/"); + navigate("/home"); }) .catch((error) => { console.error('Error deleting insurance data:', error); @@ -99,8 +99,8 @@ function EditPage() { handleClose={handleCloseModal} handleConfirm={handleConfirmDelete} /> -

    Versicherung bearbeiten

    -
    +

    Versicherung bearbeiten

    +
    ([]); @@ -27,20 +25,17 @@ function HomePage() { }, []); return ( - <> -

    Übersicht

    -
    - - - -
    +
    + + +
    - +
    ); } diff --git a/frontend/src/pages/AddPage.css b/frontend/src/pages/SharedComponents.css similarity index 54% rename from frontend/src/pages/AddPage.css rename to frontend/src/pages/SharedComponents.css index 8509207..223099f 100644 --- a/frontend/src/pages/AddPage.css +++ b/frontend/src/pages/SharedComponents.css @@ -29,61 +29,95 @@ .form-section input, .form-section select, .form-section textarea { - width: calc(100% - 10px); + width: 100%; padding: 10px; box-sizing: border-box; margin-bottom: -10px; } +.form-section input { + padding-right: 140px; +} + .form-section input[name="type"] { visibility: hidden; } -.button-container { - margin-top: 10px; - text-align: center; +.overview-container { + display: flex; + flex-direction: column; + align-items: center; } -.button-save { - width: 250px; - height: 40px; - position: absolute; - left: 50%; - bottom: -22%; - transform: translateX(-50%); - color: #f5f5f5; - box-shadow: 2px 4px 4px rgba(0, 0, 0, 0.1); +.overview-title { + margin-bottom: 60px; } -.button-add { - width: 250px; - height: 40px; - position: absolute; - margin-top: 130px; - transform: translateY(-210%); - right: 3%; - box-shadow: 2px 4px 4px rgba(0, 0, 0, 0.1); +.accordion-button, +.sort-button { + background-color: transparent; + border: none; + cursor: pointer; + color: white; + width: 32px; + height: 32px; + font-size: 14px; + font-weight: bold; + text-align: right; + box-shadow: 2px 3px 4px rgba(0, 0, 0, 0.1); } -.button-add a { - color: white; +.accordion-button { + margin-right: 10px; } -.button-add:hover a { - color: #f5f5f5; - text-decoration: none; +.sort-button { + margin-right: -5px; } -.overview-container { - position: absolute; - top: 100px; - left: 50%; - transform: translateX(-50%); - display: flex; - flex-direction: column; - align-items: center; +.asc-icon::before, +.desc-icon::before { + display: inline-block; + vertical-align: middle; + line-height: 1; } -.overview-title { - margin-bottom: 60px; +.asc-icon::before { + content: '\25B4'; + font-weight: bold; + font-size: 24px; } + +.desc-icon::before { + content: '\25BE'; + font-weight: bold; + font-size: 24px; +} + +.plus::before { + content: '\002B'; + font-weight: bold; + font-size: 24px; +} + +.minus::before { + content: '\2212'; + font-weight: bold; + font-size: 24px; +} + +.button-container { + margin-top: 10px; + text-align: center; +} + +.button-save { + width: 250px; + height: 40px; + position: absolute; + left: 50%; + bottom: 5%; + transform: translateX(-50%); + color: #f5f5f5; + box-shadow: 2px 4px 4px rgba(0, 0, 0, 0.1); +} \ No newline at end of file diff --git a/frontend/src/types/types.ts b/frontend/src/types/types.ts index 37a20e5..87df668 100644 --- a/frontend/src/types/types.ts +++ b/frontend/src/types/types.ts @@ -59,4 +59,13 @@ export type AllInsurancesResponse = { lifeInsurances: Insurance[]; propertyInsurances: Insurance[]; vehicleInsurances: Insurance[]; +}; + +export type AppUser = { + id: string, + login: string, +}; + +export type ProtectedRoutesProps = { + appUser: AppUser | null | undefined; }; \ No newline at end of file