diff --git a/.gitignore b/.gitignore index 3e189ed..170ba0d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .env .idea -keycloak/CAS/target \ No newline at end of file +keycloak/CAS/target +docker-compose.yaml diff --git a/Makefile b/Makefile index a5606be..6944ef4 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ help: clean: @cp config/prod.docker-compose.yaml docker-compose.yaml - @docker compose down 2> /dev/null + @docker compose down @rm -f docker-compose.yaml @rm -f .env @rm -f front/MyINPulse-front/.env @@ -21,14 +21,14 @@ dev-front: clean vite @cp config/frontdev.front.env front/MyINPulse-front/.env @cp config/frontdev.main.env .env @cp config/frontdev.docker-compose.yaml docker-compose.yaml - @docker compose up -d + @docker compose up -d --build @cd ./front/MyINPulse-front/ && npm run dev prod: clean @cp config/prod.front.env front/MyINPulse-front/.env @cp config/prod.main.env .env @cp config/frontdev.docker-compose.yaml docker-compose.yaml - @docker compose up -d + @docker compose up -d --build @@ -36,6 +36,6 @@ dev-back: @cp config/backdev.front.env front/MyINPulse-front/.env @cp config/backdev.main.env .env @cp config/backdev.docker-compose.yaml docker-compose.yaml - @docker compose up -d + @docker compose up -d --build @echo "cd MyINPulse-back" @echo "./gradlew bootRun --args='--server.port=8081'" \ No newline at end of file diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/MyinpulseApplication.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/MyinpulseApplication.java index b4b5e7b..23c2f28 100644 --- a/MyINPulse-back/src/main/java/enseirb/myinpulse/MyinpulseApplication.java +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/MyinpulseApplication.java @@ -1,22 +1,22 @@ package enseirb.myinpulse; +import enseirb.myinpulse.security.KeycloakJwtRolesConverter; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; -import org.springframework.security.config.Customizer; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.builders.WebSecurity; -import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer; +import org.springframework.security.oauth2.jwt.*; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import java.util.Arrays; +import java.util.*; +import java.util.stream.Collectors; -import static org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope; +import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole; @SpringBootApplication public class MyinpulseApplication { @@ -25,34 +25,5 @@ public class MyinpulseApplication { SpringApplication.run(MyinpulseApplication.class, args); } - // CORS configuration - // TODO: make sure to only accept our own domains - @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(Arrays.asList("*")); - configuration.setAllowedMethods(Arrays.asList("GET", "OPTIONS")); - configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", - "x-auth-token")); // Do not remove, this fixes the CORS errors when unauthenticated - //configuration.setExposedHeaders(Arrays.asList("x-auth-token")); - UrlBasedCorsConfigurationSource source = new - UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - - return source; - } - - // TODO: add unauthenticated endpoint with server status - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/random2").access(hasScope("contacts")) - .requestMatchers("/getUserInfo").access(hasScope("messages")) - .anyRequest().authenticated() - ) - .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults())); - return http.build(); - } } diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/config/WebSecurityCustomConfiguration.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/config/WebSecurityCustomConfiguration.java new file mode 100644 index 0000000..14c46b3 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/config/WebSecurityCustomConfiguration.java @@ -0,0 +1,50 @@ +package enseirb.myinpulse.config; + +import enseirb.myinpulse.security.KeycloakJwtRolesConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; +import java.util.List; + +import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole; + +@Configuration +public class WebSecurityCustomConfiguration { + // CORS configuration + // TODO: make sure to only accept our own domains + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(List.of("*")); + configuration.setAllowedMethods(Arrays.asList("GET", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", + "x-auth-token")); // Do not remove, this fixes the CORS errors when unauthenticated + UrlBasedCorsConfigurationSource source = new + UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + + return source; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/random2").access(hasRole("REALM_MyINPulse-entrepreneur")) + .requestMatchers("/random").access(hasRole("REALM_MyINPulse-admin")) + .requestMatchers("/random3").permitAll() + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt. + jwtAuthenticationConverter(new KeycloakJwtRolesConverter()))); + return http.build(); + + } +} \ No newline at end of file diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/security/KeycloakJwtRolesConverter.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/security/KeycloakJwtRolesConverter.java new file mode 100644 index 0000000..fafbef5 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/security/KeycloakJwtRolesConverter.java @@ -0,0 +1,98 @@ +package enseirb.myinpulse.security; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toSet; + + +public class KeycloakJwtRolesConverter implements Converter { + /** + * Prefix used for realm level roles. + */ + public static final String PREFIX_REALM_ROLE = "ROLE_REALM_"; + /** + * Prefix used in combination with the resource (client) name for resource level roles. + */ + public static final String PREFIX_RESOURCE_ROLE = "ROLE_"; + + /** + * Name of the claim containing the realm level roles + */ + private static final String CLAIM_REALM_ACCESS = "realm_access"; + /** + * Name of the claim containing the resources (clients) the user has access to. + */ + private static final String CLAIM_RESOURCE_ACCESS = "resource_access"; + /** + * Name of the claim containing roles. (Applicable to realm and resource level.) + */ + private static final String CLAIM_ROLES = "roles"; + + @Override + public AbstractAuthenticationToken convert(Jwt source) + { + return new JwtAuthenticationToken(source, Stream.concat(new JwtGrantedAuthoritiesConverter().convert(source) + .stream(), TEMPORARNAME(source).stream()) + .collect(toSet())); + } + + /** + * Extracts the realm and resource level roles from a JWT token distinguishing between them using prefixes. + */ + public Collection TEMPORARNAME(Jwt jwt) { + // Collection that will hold the extracted roles + Collection grantedAuthorities = new ArrayList<>(); + + // Realm roles + // Get the part of the access token that holds the roles assigned on realm level + Map> realmAccess = jwt.getClaim(CLAIM_REALM_ACCESS); + + // Verify that the claim exists and is not empty + if (realmAccess != null && !realmAccess.isEmpty()) { + // From the realm_access claim get the roles + Collection roles = realmAccess.get(CLAIM_ROLES); + // Check if any roles are present + if (roles != null && !roles.isEmpty()) { + // Iterate of the roles and add them to the granted authorities + Collection realmRoles = roles.stream() + // Prefix all realm roles with "ROLE_realm_" + .map(role -> new SimpleGrantedAuthority(PREFIX_REALM_ROLE + role)) + .collect(Collectors.toList()); + grantedAuthorities.addAll(realmRoles); + } + } + + // Resource (client) roles + // A user might have access to multiple resources all containing their own roles. Therefore, it is a map of + // resource each possibly containing a "roles" property. + Map>> resourceAccess = jwt.getClaim(CLAIM_RESOURCE_ACCESS); + + // Check if resources are assigned + if (resourceAccess != null && !resourceAccess.isEmpty()) { + // Iterate of all the resources + resourceAccess.forEach((resource, resourceClaims) -> { + // Iterate of the "roles" claim inside the resource claims + resourceClaims.get(CLAIM_ROLES).forEach( + // Add the role to the granted authority prefixed with ROLE_ and the name of the resource + role -> grantedAuthorities.add(new SimpleGrantedAuthority(PREFIX_RESOURCE_ROLE + resource + "_" + role)) + ); + }); + } + + return grantedAuthorities; + } + + +} diff --git a/MyINPulse-back/src/main/resources/application.properties b/MyINPulse-back/src/main/resources/application.properties index 9264594..6d6825f 100644 --- a/MyINPulse-back/src/main/resources/application.properties +++ b/MyINPulse-back/src/main/resources/application.properties @@ -1,3 +1,4 @@ spring.application.name=myinpulse spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:7080/realms/test/protocol/openid-connect/certs -spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:7080/realms/test \ No newline at end of file +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:7080/realms/test +logging.level.org.springframework.security=DEBUG diff --git a/config/backdev.front.env b/config/backdev.front.env index 9db91ea..27cf54e 100644 --- a/config/backdev.front.env +++ b/config/backdev.front.env @@ -2,3 +2,4 @@ VITE_KEYCLOAK_URL=http://localhost:7080 VITE_KEYCLOAK_CLIENT_ID=myinpulse VITE_KEYCLOAK_REALM=test VITE_APP_URL=http://localhost:8080 +VITE_BACKEND_URL=http://localhost:8081/ diff --git a/config/frontdev.front.env b/config/frontdev.front.env index d6b7016..5eba221 100644 --- a/config/frontdev.front.env +++ b/config/frontdev.front.env @@ -1,4 +1,5 @@ VITE_KEYCLOAK_URL=http://localhost:7080 -VITE_KEYCLOAK_CLIENT_ID=myinpulse +VITE_KEYCLOAK_CLIENT_ID=myinpulse-dev VITE_KEYCLOAK_REALM=test VITE_APP_URL=http://localhost:5173 +VITE_BACKEND_URL=http://localhost:8081/ diff --git a/config/prod.front.env b/config/prod.front.env index d5007dc..cb42a37 100644 --- a/config/prod.front.env +++ b/config/prod.front.env @@ -2,3 +2,4 @@ VITE_KEYCLOAK_URL=https://0549cd63f912d5dc9b31278d6f.eirb.fr VITE_KEYCLOAK_CLIENT_ID=myinpulse-eirb VITE_KEYCLOAK_REALM=test VITE_APP_URL=https://0549cd63f912d5dc9b31278d6f.piair.dev +VITE_BACKEND_URL=http://TODO/ diff --git a/front/Dockerfile b/front/Dockerfile index 14faeb6..2a8d3c0 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -36,4 +36,4 @@ RUN chmod +x /start.sh EXPOSE 80 # Set the entry point to the shell script -ENTRYPOINT ["/start.sh"] \ No newline at end of file +ENTRYPOINT ["/start.sh"] diff --git a/front/MyINPulse-front/src/App.vue b/front/MyINPulse-front/src/App.vue index f2db102..4977758 100644 --- a/front/MyINPulse-front/src/App.vue +++ b/front/MyINPulse-front/src/App.vue @@ -3,11 +3,13 @@ import { RouterLink, RouterView } from 'vue-router' import HelloWorld from './components/HelloWorld.vue' import Header from './components/Header.vue'; import ProjectComp from './components/Project-comp.vue'; +import CanvasView from './components/canvas/Lean-canvas.vue'; +import ErrorWrapper from "@/views/errorWrapper.vue"; - - - - diff --git a/front/MyINPulse-front/src/components/TheWelcome.vue b/front/MyINPulse-front/src/components/TheWelcome.vue deleted file mode 100644 index 674b490..0000000 --- a/front/MyINPulse-front/src/components/TheWelcome.vue +++ /dev/null @@ -1,94 +0,0 @@ - - - diff --git a/front/MyINPulse-front/src/components/WelcomeItem.vue b/front/MyINPulse-front/src/components/WelcomeItem.vue deleted file mode 100644 index 6d7086a..0000000 --- a/front/MyINPulse-front/src/components/WelcomeItem.vue +++ /dev/null @@ -1,87 +0,0 @@ - - - diff --git a/front/MyINPulse-front/src/components/canvas/Avantage.vue b/front/MyINPulse-front/src/components/canvas/Avantage.vue new file mode 100644 index 0000000..59532ce --- /dev/null +++ b/front/MyINPulse-front/src/components/canvas/Avantage.vue @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/canvas/Canaux.vue b/front/MyINPulse-front/src/components/canvas/Canaux.vue new file mode 100644 index 0000000..6aee546 --- /dev/null +++ b/front/MyINPulse-front/src/components/canvas/Canaux.vue @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/canvas/Couts.vue b/front/MyINPulse-front/src/components/canvas/Couts.vue new file mode 100644 index 0000000..c232d3f --- /dev/null +++ b/front/MyINPulse-front/src/components/canvas/Couts.vue @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/canvas/Header.vue b/front/MyINPulse-front/src/components/canvas/Header.vue new file mode 100644 index 0000000..e860400 --- /dev/null +++ b/front/MyINPulse-front/src/components/canvas/Header.vue @@ -0,0 +1,37 @@ + + + + + + \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/canvas/Indicateurs.vue b/front/MyINPulse-front/src/components/canvas/Indicateurs.vue new file mode 100644 index 0000000..0fd6d1a --- /dev/null +++ b/front/MyINPulse-front/src/components/canvas/Indicateurs.vue @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/canvas/Lean-canvas.vue b/front/MyINPulse-front/src/components/canvas/Lean-canvas.vue new file mode 100644 index 0000000..0141a17 --- /dev/null +++ b/front/MyINPulse-front/src/components/canvas/Lean-canvas.vue @@ -0,0 +1,42 @@ + + + + + + \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/canvas/Probleme.vue b/front/MyINPulse-front/src/components/canvas/Probleme.vue new file mode 100644 index 0000000..d36b983 --- /dev/null +++ b/front/MyINPulse-front/src/components/canvas/Probleme.vue @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/canvas/Revenus.vue b/front/MyINPulse-front/src/components/canvas/Revenus.vue new file mode 100644 index 0000000..0b8b57e --- /dev/null +++ b/front/MyINPulse-front/src/components/canvas/Revenus.vue @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/canvas/Segments.vue b/front/MyINPulse-front/src/components/canvas/Segments.vue new file mode 100644 index 0000000..76eaba5 --- /dev/null +++ b/front/MyINPulse-front/src/components/canvas/Segments.vue @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/canvas/Solution.vue b/front/MyINPulse-front/src/components/canvas/Solution.vue new file mode 100644 index 0000000..b3d5a05 --- /dev/null +++ b/front/MyINPulse-front/src/components/canvas/Solution.vue @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/canvas/Valeur.vue b/front/MyINPulse-front/src/components/canvas/Valeur.vue new file mode 100644 index 0000000..d577863 --- /dev/null +++ b/front/MyINPulse-front/src/components/canvas/Valeur.vue @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/canvas/style-project.css b/front/MyINPulse-front/src/components/canvas/style-project.css new file mode 100644 index 0000000..7761d40 --- /dev/null +++ b/front/MyINPulse-front/src/components/canvas/style-project.css @@ -0,0 +1,184 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 10px; + } + + .canvas { + display: flex; + flex-direction: column; + max-width: 1200px; + margin: 20px auto; + border: 2px dashed #d33; + background: #fff; + } + + .row { + display: flex; + } + + .cell { + flex: 1; + border: 1px solid #ddd; + padding: 10px; + text-align: center; + background-color: #f1f1f1; + } + + .produit { + background-color: #f9e4e4; + } + + .marche { + background-color: #e4f1f9; + } + + .valeur { + background-color: #f9f4e4; + } + + h3 { + margin: 0; + font-size: 18px; + color: #333; + } + + p { + margin: 5px 0 0; + font-size: 14px; + } + + + body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #f9f9f9; + } + + h1 img { + height: 80px; + } + + .canvas { + max-width: 1200px; + margin: 20px auto; + padding: 20px; + border: 2px dashed #d33; + background-color: #fff; + } + + .row { + display: flex; + margin-bottom: 10px; + } + + .cell { + flex: 1; + border: 1px solid #ddd; + padding: 20px; + text-align: center; + background-color: #f1f1f1; + } + + #ade { + max-width: 1200px; + margin: 20px auto; + padding: 20px; + text-align: center; + background-color: #e8f5e9; + border: 2px solid #4caf50; + border-radius: 10px; + } + + #ade h3 { + color: #2e7d32; + } + + #ade p { + margin: 10px 0; + font-size: 16px; + color: #333; + } + header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 20px; + background-color: #fff; + border-bottom: 2px solid #ddd; + } + + header img { + height: 60px; + } + + header .contact-menu { + position: relative; + } + + .expanded { + transform: scale(1.2); /* L'élément reste agrandi */ + transition: transform 0.3s ease; /* Animation fluide */ + } + + .contact-button, .return { + padding: 10px 15px; + border: none; + border-radius: 4px; + background-color: #2196f3; + color: #fff; + cursor: pointer; + } + + .contact-button:hover, .return:hover { + background-color: #1976d2; + } + + /* Dropdown styling */ + .contact-dropdown { + position: absolute; + right: 0; + top: 50px; + display: none; + flex-direction: column; + gap: 10px; + padding: 15px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + z-index: 10; + } + + .contact-dropdown button { + padding: 8px 12px; + border: none; + border-radius: 4px; + background-color: #4caf50; + color: #fff; + cursor: pointer; + } + + .contact-dropdown button:hover { + background-color: #388e3c; + } + + .return { + background-color: #f44336; + } + + .return:hover { + background-color: #d32f2f; + } + + .header-buttons { + display: flex; + align-items: center; + gap: 15px; + } + + + a{ + color: white; + } \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/errorModal.vue b/front/MyINPulse-front/src/components/errorModal.vue new file mode 100644 index 0000000..0d7dd57 --- /dev/null +++ b/front/MyINPulse-front/src/components/errorModal.vue @@ -0,0 +1,94 @@ + + + + + \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/icons/IconCommunity.vue b/front/MyINPulse-front/src/components/icons/IconCommunity.vue deleted file mode 100644 index 2dc8b05..0000000 --- a/front/MyINPulse-front/src/components/icons/IconCommunity.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/front/MyINPulse-front/src/components/icons/IconDocumentation.vue b/front/MyINPulse-front/src/components/icons/IconDocumentation.vue deleted file mode 100644 index 6d4791c..0000000 --- a/front/MyINPulse-front/src/components/icons/IconDocumentation.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/front/MyINPulse-front/src/components/icons/IconEcosystem.vue b/front/MyINPulse-front/src/components/icons/IconEcosystem.vue deleted file mode 100644 index c3a4f07..0000000 --- a/front/MyINPulse-front/src/components/icons/IconEcosystem.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/front/MyINPulse-front/src/components/icons/IconSupport.vue b/front/MyINPulse-front/src/components/icons/IconSupport.vue deleted file mode 100644 index 7452834..0000000 --- a/front/MyINPulse-front/src/components/icons/IconSupport.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/front/MyINPulse-front/src/components/icons/IconTooling.vue b/front/MyINPulse-front/src/components/icons/IconTooling.vue deleted file mode 100644 index 660598d..0000000 --- a/front/MyINPulse-front/src/components/icons/IconTooling.vue +++ /dev/null @@ -1,19 +0,0 @@ - - diff --git a/front/MyINPulse-front/src/helpers.ts b/front/MyINPulse-front/src/helpers.ts deleted file mode 100644 index 0bd894a..0000000 --- a/front/MyINPulse-front/src/helpers.ts +++ /dev/null @@ -1,26 +0,0 @@ -import axios from "axios"; - -const backendUrl = "http://localhost:8080/" - -// TODO: spawn a error modal -function defaultApiErrorHandler(err: String){ - console.log(err) -} - -function defaultApiSuccessHandler(response: () => void){ - console.log(response) -} -/* -function callApi(endpoint: string, onSuccessHandler: () => void, onErrorHandler: (err: String) => void): void { - console.log("callApi: "+ endpoint) - axios.get(backendUrl + endpoint).then( - onSuccessHandler == null ? defaultApiSuccessHandler : onSuccessHandler - ).catch( - (err) => { - onErrorHandler == null ? defaultApiErrorHandler(err): onErrorHandler(err); - } - ) -} - -export {callApi} - */ \ No newline at end of file diff --git a/front/MyINPulse-front/src/main.ts b/front/MyINPulse-front/src/main.ts index e33d5c1..60acf0b 100644 --- a/front/MyINPulse-front/src/main.ts +++ b/front/MyINPulse-front/src/main.ts @@ -1,31 +1,36 @@ import { createApp } from 'vue' import App from './App.vue' -import router from './router' -//import VueKeyCloak from '@dsb-norge/vue-keycloak-js' -//import { useKeycloak } from '@dsb-norge/vue-keycloak-js' -//import axios from 'axios' +import router from './router/router.ts' import {createPinia} from "pinia"; import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; import AuthStorePlugin from './plugins/authStore'; import keycloakService from './services/keycloak'; +import {useAuthStore} from "@/stores/authStore.ts"; +let store: any; keycloakService.CallInit(() => { + try { + const app = createApp(App) - const app = createApp(App) + // Setup pinia store, allowing user to keep logged in status after refresh + const pinia = createPinia(); + pinia.use(piniaPluginPersistedstate); + app.use(pinia); + app.use(AuthStorePlugin, { pinia }); + store = useAuthStore(); + app.use(router) - -// Setup pinia store, allowing user to keep logged in status after refresh - const pinia = createPinia(); - pinia.use(piniaPluginPersistedstate); - - app.use(pinia); - app.use(AuthStorePlugin, { pinia }); - app.use(router) - app.mount('#app'); + app.mount('#app'); + } catch (e) { + console.error("Error while initiating Keycloak.") + console.error(e) + createApp(App).mount('#app'); + } }) -createApp(App).mount('#app'); +// this shit made by me so i can run the canva vue app +createApp(App).use(router).mount('#app'); // TODO: fix the comment /* @@ -67,3 +72,6 @@ app.use(VueKeyCloak,{ */ + + +export {store}; \ No newline at end of file diff --git a/front/MyINPulse-front/src/router/index.ts b/front/MyINPulse-front/src/router/router.ts similarity index 57% rename from front/MyINPulse-front/src/router/index.ts rename to front/MyINPulse-front/src/router/router.ts index e9697d9..16f4939 100644 --- a/front/MyINPulse-front/src/router/index.ts +++ b/front/MyINPulse-front/src/router/router.ts @@ -3,21 +3,20 @@ import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ - { - path: '/about', - name: 'about', - // route level code-splitting - // this generates a separate chunk (About.[hash].js) for this route - // which is lazy-loaded when the route is visited. - component: () => import('../views/AboutView.vue'), - }, { path: '/test', name: 'test', // route level code-splitting // this generates a separate chunk (About.[hash].js) for this route // which is lazy-loaded when the route is visited. - component: () => import('../views/Home.vue'), + component: () => import('../views/test.vue'), + }, + + // route pour les canvas (made by adnane), in fact the two vue apps are separated for now + { + path: '/canvas', + name: 'canvas', + component: () => import('../views/CanvasView.vue'), }, ], }) diff --git a/front/MyINPulse-front/src/services/api.ts b/front/MyINPulse-front/src/services/api.ts index eb75333..cea6a9e 100644 --- a/front/MyINPulse-front/src/services/api.ts +++ b/front/MyINPulse-front/src/services/api.ts @@ -1,13 +1,56 @@ -// file: src/services/api.js - import axios from "axios"; +import {store} from "@/main.ts"; +import {addNewMessage, color} from "@/services/popupDisplayer.ts"; -// Creating an instance for axios to be used by the token interceptor service -const instance = axios.create({ - baseURL: `${import.meta.env.VITE_BE_API_URL}/api`, +const axiosInstance = axios.create({ + baseURL: import.meta.env.VITE_BACKEND_URL, headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, }); -export default instance; \ No newline at end of file +axiosInstance.interceptors.response.use( + response => response, // Directly return successful responses. + async error => { + const originalRequest = error.config; + if (error.response.status === 401 && !originalRequest._retry && store.authenticated) { + originalRequest._retry = true; // Mark the request as retried to avoid infinite loops. + try { + await store.refreshUserToken(); + // Update the authorization header with the new access token. + axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${store.user.token}`; + return axiosInstance(originalRequest); // Retry the original request with the new access token. + } catch (refreshError) { + // Handle refresh token errors by clearing stored tokens and redirecting to the login page. + console.error('Token refresh failed:', refreshError); + localStorage.removeItem('accessToken'); + localStorage.removeItem('refreshToken'); + window.location.href = '/login'; + return Promise.reject(refreshError); + } + } + return Promise.reject(error); // For all other errors, return the error as is. + } +); + +// TODO: spawn a error modal +function defaultApiErrorHandler(err: string){ + addNewMessage(err, color.Red); +} + +function defaultApiSuccessHandler(response: any){ + addNewMessage(response.data, color.green) +} +function callApi(endpoint: string, onSuccessHandler?: any, onErrorHandler?: any): void { + axiosInstance.get(endpoint).then( + onSuccessHandler == null ? defaultApiSuccessHandler : onSuccessHandler + ).catch( + (err) => { + onErrorHandler == null ? defaultApiErrorHandler(err): onErrorHandler(err); + throw err; + } + ) +} + + +export {callApi} \ No newline at end of file diff --git a/front/MyINPulse-front/src/services/popupDisplayer.ts b/front/MyINPulse-front/src/services/popupDisplayer.ts new file mode 100644 index 0000000..3b222a2 --- /dev/null +++ b/front/MyINPulse-front/src/services/popupDisplayer.ts @@ -0,0 +1,19 @@ +import {ref} from "vue"; +enum color {Red, Yellow, Blue, green} + +function addNewMessage(errorMessage: string, type?: color, timeout?: number){ + if (timeout == null){ + timeout = 5000; + } + if (type == null){ + type = color.Red; + } + + const data = {errorMessage: errorMessage, timeout: timeout, type: type, uid: Math.random()*100000}; + errorList.value.push(data) + setTimeout(() => errorList.value.slice(0, 1), timeout) +} + +const errorList: any= ref([]) + +export {addNewMessage, errorList, color} \ No newline at end of file diff --git a/front/MyINPulse-front/src/views/CanvasView.vue b/front/MyINPulse-front/src/views/CanvasView.vue new file mode 100644 index 0000000..d87bd29 --- /dev/null +++ b/front/MyINPulse-front/src/views/CanvasView.vue @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/front/MyINPulse-front/src/views/errorModal.vue b/front/MyINPulse-front/src/views/errorModal.vue deleted file mode 100644 index 92722f1..0000000 --- a/front/MyINPulse-front/src/views/errorModal.vue +++ /dev/null @@ -1,32 +0,0 @@ - - - - - \ No newline at end of file diff --git a/front/MyINPulse-front/src/views/errorWrapper.vue b/front/MyINPulse-front/src/views/errorWrapper.vue new file mode 100644 index 0000000..1d56a2a --- /dev/null +++ b/front/MyINPulse-front/src/views/errorWrapper.vue @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/front/MyINPulse-front/src/views/Home.vue b/front/MyINPulse-front/src/views/test.vue similarity index 57% rename from front/MyINPulse-front/src/views/Home.vue rename to front/MyINPulse-front/src/views/test.vue index 7fbe869..90db6d1 100644 --- a/front/MyINPulse-front/src/views/Home.vue +++ b/front/MyINPulse-front/src/views/test.vue @@ -1,7 +1,16 @@ @@ -26,19 +35,26 @@ const store = useAuthStore() {{store.user.refreshToken}} - Unauthenticated API call - + Entrepreneur API call + res - Authenticated API call - + Admin API call + res + + Unauth API call + + res + + +