diff --git a/.gitea/workflows/back.yaml b/.gitea/workflows/back.yaml new file mode 100644 index 0000000..bde3a22 --- /dev/null +++ b/.gitea/workflows/back.yaml @@ -0,0 +1,18 @@ +name: Format + +on: [ push, pull_request ] + +jobs: + + formatting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 # v2 minimum required + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' # See 'Supported distributions' for available options + java-version: '21' + + - uses: axel-op/googlejavaformat-action@v3 + with: + args: "--set-exit-if-changed --skip-sorting-imports --skip-reflowing-long-strings --aosp -n" diff --git a/.gitea/workflows/build-back.yaml b/.gitea/workflows/build-back.yaml new file mode 100644 index 0000000..2526e98 --- /dev/null +++ b/.gitea/workflows/build-back.yaml @@ -0,0 +1,33 @@ +name: Build + +on: + push: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Load .env file + uses: xom9ikk/dotenv@v2.3.0 + with: + path: ./config/ + mode: dev + load-mode: strict + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 21 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-disabled: true # Once the code has been pushed once in main, this should be reenabled. + + - name: init gradle + working-directory: ./MyINPulse-back/ + run: ./gradlew build # todo: run test, currently fail because no database is present diff --git a/.gitignore b/.gitignore index 170ba0d..7117a86 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .idea keycloak/CAS/target docker-compose.yaml +postgres/data \ No newline at end of file diff --git a/Makefile b/Makefile index 6944ef4..6f4e820 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,16 @@ help: - @echo "make [clean dev-front prod dev-back]" + @echo "make [clean dev-front prod dev-back dev]" clean: + @cp config/frontdev.env front/MyINPulse-front/.env + @cp config/frontdev.env .env + @cp config/frontdev.env MyINPulse-back/.env @cp config/prod.docker-compose.yaml docker-compose.yaml @docker compose down @rm -f docker-compose.yaml @rm -f .env @rm -f front/MyINPulse-front/.env - + @rm -f MyINPulse-back/.env # Install npm packages front/MyINPulse-front/.installed: @@ -16,26 +19,55 @@ front/MyINPulse-front/.installed: vite: ./front/MyINPulse-front/.installed +keycloak: ./keycloak/.installed -dev-front: clean vite - @cp config/frontdev.front.env front/MyINPulse-front/.env - @cp config/frontdev.main.env .env +keycloak/.installed: + @echo "running one time install" + @cd keycloak/CAS && sudo sh build.sh + @touch ./keycloak/.installed + +dev-front: clean vite keycloak + @cp config/frontdev.env front/MyINPulse-front/.env + @cp config/frontdev.env .env + @cp config/frontdev.env MyINPulse-back/.env @cp config/frontdev.docker-compose.yaml docker-compose.yaml @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 +prod: clean keycloak + @cp config/prod.env front/MyINPulse-front/.env + @cp config/prod.env .env + @cp config/prod.env .env + @cp config/prod.docker-compose.yaml docker-compose.yaml @docker compose up -d --build -dev-back: - @cp config/backdev.front.env front/MyINPulse-front/.env - @cp config/backdev.main.env .env +dev-back: keycloak + @cp config/backdev.env front/MyINPulse-front/.env + @cp config/backdev.env .env + @cp config/backdev.env MyINPulse-back/.env @cp config/backdev.docker-compose.yaml docker-compose.yaml @docker compose up -d --build - @echo "cd MyINPulse-back" - @echo "./gradlew bootRun --args='--server.port=8081'" \ No newline at end of file + @echo "cd MyINPulse-back" && echo 'export $$(cat .env | xargs)' + @echo "./gradlew bootRun --args='--server.port=8081'" + +dev: clean vite keycloak + @cp config/dev.env front/MyINPulse-front/.env + @cp config/dev.env .env + @cp config/dev.env MyINPulse-back/.env + @cp config/dev.docker-compose.yaml docker-compose.yaml + @docker compose up -d --build + @echo "cd MyINPulse-back" && echo 'export $$(cat .env | xargs)' + @echo "./gradlew bootRun --args='--server.port=8081'" + @cd ./front/MyINPulse-front/ && npm run dev & + +test-back: clean keycloak + @cp config/dev.env front/MyINPulse-front/.env + @cp config/dev.env .env + @cp config/dev.env MyINPulse-back/.env + @cp config/dev.docker-compose.yaml docker-compose.yaml + @docker compose up -d --build + @echo "cd MyINPulse-back" && echo 'export $$(cat .env | xargs)' + @cd ./MyINPulse-back/ && ./gradlew test && ./gradlew jacocoTestReport + @firefox ./MyINPulse-back/build/jacocoHtml/index.html diff --git a/MyINPulse-back/build.gradle b/MyINPulse-back/build.gradle index abdd6a9..47b0dc2 100644 --- a/MyINPulse-back/build.gradle +++ b/MyINPulse-back/build.gradle @@ -1,29 +1,59 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.4.2' - id 'io.spring.dependency-management' version '1.1.7' + id 'java' + id 'org.springframework.boot' version '3.4.2' + id 'io.spring.dependency-management' version '1.1.7' + id 'jacoco' } group = 'enseirb' version = '0.0.1-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' - implementation 'org.springframework.boot:spring-boot-starter-web' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-data-rest' + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.+' + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.+' + implementation 'org.postgresql:postgresql' + implementation group: 'com.itextpdf', name: 'itextpdf', version: '5.5.13.3' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'com.h2database:h2' + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() +} + + +test { + finalizedBy jacocoTestReport // report is always generated after tests run +} +jacocoTestReport { + dependsOn test // tests are required to run before generating the report + reports { + xml.required = false + csv.required = false + html.outputLocation = layout.buildDirectory.dir('jacocoHtml') + } +} + + +jacoco { + toolVersion = "0.8.12" + reportsDirectory = layout.buildDirectory.dir('customJacocoReportDir') } diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/MyinpulseApplication.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/MyinpulseApplication.java index 23c2f28..196569d 100644 --- a/MyINPulse-back/src/main/java/enseirb/myinpulse/MyinpulseApplication.java +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/MyinpulseApplication.java @@ -1,29 +1,15 @@ 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.core.convert.converter.Converter; -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; 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 java.util.*; -import java.util.stream.Collectors; - -import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole; @SpringBootApplication public class MyinpulseApplication { - public static void main(String[] args) { - SpringApplication.run(MyinpulseApplication.class, args); - } - - + public static void main(String[] args) { + SpringApplication.run(MyinpulseApplication.class, args); + } } diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/api/GetUserInfo.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/api/GetUserInfo.java deleted file mode 100644 index 50262e1..0000000 --- a/MyINPulse-back/src/main/java/enseirb/myinpulse/api/GetUserInfo.java +++ /dev/null @@ -1,43 +0,0 @@ -package enseirb.myinpulse.api; - -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.bind.annotation.CrossOrigin; - -import java.security.Principal; - -@SpringBootApplication -@RestController -public class GetUserInfo { - // TODO: understand how to get data - @GetMapping("/getUserInfo") - public Object user(Principal principal) { - System.out.println("GetUserInfo + " + principal); - System.out.println(SecurityContextHolder.getContext().getAuthentication()); - return SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - } - - @CrossOrigin(methods = {RequestMethod.GET, RequestMethod.OPTIONS}) - @GetMapping("/random") - public boolean rand(){ - System.err.println("HELLO"); - return Math.random() > 0.5; - } - - @CrossOrigin(methods = {RequestMethod.GET, RequestMethod.OPTIONS}) - @GetMapping("/random2") - public boolean rand2(){ - System.err.println("HELLO2"); - return Math.random() > 0.5; - } - - @CrossOrigin(methods = {RequestMethod.GET, RequestMethod.OPTIONS}) - @GetMapping("/random3") - public boolean rand3(){ - System.err.println("HELLO"); - return Math.random() > 0.5; - } -} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/config/KeycloakJwtRolesConverter.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/config/KeycloakJwtRolesConverter.java new file mode 100644 index 0000000..5a95ef6 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/config/KeycloakJwtRolesConverter.java @@ -0,0 +1,107 @@ +/* + * Source: https://github.com/ChristianHuff-DEV/secure-spring-rest-api-using-keycloak/blob/main/src/main/java/io/betweendata/RestApi/security/oauth2/KeycloakJwtRolesConverter.java + * edited by Pierre Tellier + */ +package enseirb.myinpulse.config; + +import static java.util.stream.Collectors.toSet; + +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; + +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(), + tokenRolesExtractor(source).stream()) + .collect(toSet())); + } + + /** + * Extracts the realm and resource level roles from a JWT token distinguishing between them + * using prefixes. + */ + public Collection tokenRolesExtractor(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/java/enseirb/myinpulse/config/WebSecurityCustomConfiguration.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/config/WebSecurityCustomConfiguration.java index 14c46b3..81f1dd8 100644 --- a/MyINPulse-back/src/main/java/enseirb/myinpulse/config/WebSecurityCustomConfiguration.java +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/config/WebSecurityCustomConfiguration.java @@ -1,6 +1,8 @@ package enseirb.myinpulse.config; -import enseirb.myinpulse.security.KeycloakJwtRolesConverter; +import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole; + +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -12,39 +14,62 @@ 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 + + @Value("${VITE_APP_URL}") + private String frontendUrl; + + /** + * Configure the CORS (Cross Origin Ressource Sharing -- a security feature) configuration. The + * only allowed website is the frontend, defined in the .env file. + * + * @return the CORS configuration used by the backend + */ @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(List.of("*")); + configuration.setAllowedOrigins(List.of(frontendUrl)); 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(); + configuration.setAllowedHeaders( + Arrays.asList("authorization", "content-type", "x-auth-token")); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } + /** + * Configure the authorisation required for each path. + * + *

admin endpoints are under /admin/* and entrepreneur are under /entrepreneur/* + * + *

If endpoints dont require authentication, they are under /unauth/ + * + * @param http automatically filled in by spring. + * @return a securityfilterchain, automatically used by spring. + * @throws Exception TODO: figure out when the exception are raised + */ @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()))); + http.authorizeHttpRequests( + authorize -> + authorize + .requestMatchers("/entrepreneur/**", "/shared/**") + .access(hasRole("REALM_MyINPulse-entrepreneur")) + .requestMatchers("/admin/**", "/shared/**") + .access(hasRole("REALM_MyINPulse-admin")) + .requestMatchers("/unauth/**") + .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/controller/AdminApi.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/controller/AdminApi.java new file mode 100644 index 0000000..d3f432a --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/controller/AdminApi.java @@ -0,0 +1,102 @@ +package enseirb.myinpulse.controller; + +import enseirb.myinpulse.model.*; +import enseirb.myinpulse.service.AdminApiService; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.*; + +@SpringBootApplication +@RestController +public class AdminApi { + + private final AdminApiService adminApiService; + + @Autowired + AdminApi(AdminApiService adminApiService) { + this.adminApiService = adminApiService; + } + + /** + * TODO: description + * + * @return a list of all project managed by the current admin user + */ + @GetMapping("/admin/projects") + public Iterable getProjects(@AuthenticationPrincipal Jwt principal) { + return adminApiService.getProjectsOfAdmin(principal.getClaimAsString("email")); + } + + /** + * TODO: Why in admin instead of shared ? + desc + * + * @return a list of upcoming appointments for the current user + */ + @GetMapping("/admin/appointments/upcoming") + public Iterable getUpcomingAppointments(@AuthenticationPrincipal Jwt principal) { + return adminApiService.getUpcomingAppointments(principal.getClaimAsString("email")); + } + + /** + * TODO: description + * + * @return a list of current unvalidated projects, waiting to be accepted + */ + @GetMapping("/admin/projects/pending") + public Iterable getPendingProjects() { + return adminApiService.getPendingProjects(); + } + + /** + * Endpoint used to make a decision about a project. + * + *

The decision must contain the administrator + * + * @return the status code of the request + */ + @PostMapping("/admin/projects/decision") + public void validateProject(@RequestBody ProjectDecision decision) { + adminApiService.validateProject(decision); + } + + /** + * Endpoint used to manually add a project by an admin + * + * @return the status code of the request + */ + @PostMapping("/admin/project/add") + public void addNewProject(@RequestBody Project project) { + adminApiService.addNewProject(project); + } + + /** + * TODO: shouldn't it be an PUT request ? / What is the rerun type + * + *

Endpoint used to add a new report to an appointment + * + * @return the status code of the request + */ + @PostMapping("/admin/appoitements/report/{appointmentId}") + public void createAppointmentReport( + @PathVariable long appointmentId, + @RequestBody Report report, + @AuthenticationPrincipal Jwt principal) { + adminApiService.createAppointmentReport( + appointmentId, report, principal.getClaimAsString("email")); + } + + /** + * TODO: Shouldn't a project be kept in history ? 2 different endpoints ? + * + *

Endpoint used to completely remove a project. + * + * @return the status code of the request + */ + @DeleteMapping("/admin/projects/remove/{projectId}") + public void deleteProject(@PathVariable long projectId) { + adminApiService.deleteProject(projectId); + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/controller/EntrepreneurApi.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/controller/EntrepreneurApi.java new file mode 100644 index 0000000..b0de5b7 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/controller/EntrepreneurApi.java @@ -0,0 +1,78 @@ +package enseirb.myinpulse.controller; + +import enseirb.myinpulse.model.SectionCell; +import enseirb.myinpulse.model.Project; +import enseirb.myinpulse.service.EntrepreneurApiService; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.*; + +@SpringBootApplication +@RestController +public class EntrepreneurApi { + + private final EntrepreneurApiService entrepreneurApiService; + + @Autowired + EntrepreneurApi(EntrepreneurApiService entrepreneurApiService) { + this.entrepreneurApiService = entrepreneurApiService; + } + + /** + * TODO: check return type + * + *

Endpoint used to update a LC section. + * + * @return status code + */ + @PutMapping("/entrepreneur/lcsection/modify/{sectionId}") + public void editSectionCell( + @PathVariable Long sectionId, + @RequestBody SectionCell sectionCell, + @AuthenticationPrincipal Jwt principal) { + entrepreneurApiService.editSectionCell( + sectionId, sectionCell, principal.getClaimAsString("email")); + } + + /** + * TODO: checkReturn Type + * + *

Endpoint used to delete a LC section + * + * @return status code + */ + @DeleteMapping("/entrepreneur/lcsection/remove/{sectionId}") + public void removeSectionCell( + @PathVariable Long sectionId, @AuthenticationPrincipal Jwt principal) { + entrepreneurApiService.removeSectionCell(sectionId, principal.getClaimAsString("email")); + } + + /** + * TODO: check return type + * + *

Endpoint used to create a new LC section + * + * @return status code + */ + @PostMapping("/entrepreneur/lcsection/add") // remove id from doc aswell + public void addLCSection( + @RequestBody SectionCell sectionCell, @AuthenticationPrincipal Jwt principal) { + entrepreneurApiService.addSectionCell(sectionCell, principal.getClaimAsString("email")); + } + + /** + * TODO: check return type + * + *

Endpoint used to request the creation of a new project + * + * @return status code + */ + @PostMapping("/entrepreneur/project/request") + public void requestNewProject( + @RequestBody Project project, @AuthenticationPrincipal Jwt principal) { + entrepreneurApiService.requestNewProject(project, principal.getClaimAsString("email")); + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/controller/SharedApi.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/controller/SharedApi.java new file mode 100644 index 0000000..a0b63e3 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/controller/SharedApi.java @@ -0,0 +1,105 @@ +package enseirb.myinpulse.controller; + +import com.itextpdf.text.DocumentException; + +import enseirb.myinpulse.model.*; +import enseirb.myinpulse.service.SharedApiService; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.net.URISyntaxException; + +@SpringBootApplication +@RestController +public class SharedApi { + + private final SharedApiService sharedApiService; + + @Autowired + SharedApi(SharedApiService sharedApiService) { + this.sharedApiService = sharedApiService; + } + + /** + * Endpoint used to get the data inside the lean canvas + * + * @return a list of lean canvas sections + */ + @GetMapping("/shared/project/lcsection/{projectId}/{sectionId}/{date}") + public Iterable getLCSection( + @PathVariable("projectId") Long projectId, + @PathVariable("sectionId") Long sectionId, + @PathVariable("date") String date, + @AuthenticationPrincipal Jwt principal) { + return sharedApiService.getSectionCells( + projectId, sectionId, date, principal.getClaimAsString("email")); + } + + /** + * Endpoint used to get entrepreneurs details + * + * @return a list of all entrepreneurs in a project + */ + @GetMapping("/shared/entrepreneurs/{projectId}") + public Iterable getEntrepreneursByProjectId( + @PathVariable int projectId, @AuthenticationPrincipal Jwt principal) { + return sharedApiService.getEntrepreneursByProjectId( + projectId, principal.getClaimAsString("email")); + } + + /** + * Endpoint used to get the administrator of a project. + * + * @return the admin of a project + */ + @GetMapping("/shared/projects/admin/{projectId}") + public Administrator getAdminByProjectId( + @PathVariable int projectId, @AuthenticationPrincipal Jwt principal) { + return sharedApiService.getAdminByProjectId(projectId, principal.getClaimAsString("email")); + } + + /** + * Endpoint used to get all appointments of a single project. + * + * @return a list of all appointments. + */ + @GetMapping("/shared/projects/appointments/{projectId}") + public Iterable getAppointmentsByProjectId( + @PathVariable int projectId, @AuthenticationPrincipal Jwt principal) { + return sharedApiService.getAppointmentsByProjectId( + projectId, principal.getClaimAsString("email")); + } + + /** + * Endpoint used to generate a PDF report + * + * @return a PDF file? TODO: how does that works ? + */ + @GetMapping("/shared/projects/appointments/report/{appointmentId}") + public void getPDFReport( + @PathVariable int appointmentId, @AuthenticationPrincipal Jwt principal) { + try { + sharedApiService.getPDFReport(appointmentId, principal.getClaimAsString("email")); + } catch (DocumentException e) { + System.out.println(e + "Document exception"); + } catch (URISyntaxException e) { + System.out.println(e + "Error with URI"); + } catch (IOException e) { + System.out.println(e + "Failed to access file"); + } + } + + /** + * @return TODO + */ + @PostMapping("/shared/appointment/request") + public void createAppointmentRequest( + @RequestBody Appointment appointment, @AuthenticationPrincipal Jwt principal) { + sharedApiService.createAppointmentRequest(appointment, principal.getClaimAsString("email")); + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/exception/RoleNotFoudException.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/exception/RoleNotFoudException.java new file mode 100644 index 0000000..6bc4f62 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/exception/RoleNotFoudException.java @@ -0,0 +1,7 @@ +package enseirb.myinpulse.exception; + +public class RoleNotFoudException extends RuntimeException { + public RoleNotFoudException(String message) { + super(message); + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/exception/UserNotFoundException.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/exception/UserNotFoundException.java new file mode 100644 index 0000000..531be69 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/exception/UserNotFoundException.java @@ -0,0 +1,7 @@ +package enseirb.myinpulse.exception; + +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(String message) { + super(message); + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/model/Administrator.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/Administrator.java new file mode 100644 index 0000000..d6eecda --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/Administrator.java @@ -0,0 +1,66 @@ +package enseirb.myinpulse.model; + +import jakarta.persistence.*; +import jakarta.persistence.PrimaryKeyJoinColumn; +import jakarta.persistence.Table; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "administrator") +@PrimaryKeyJoinColumn(name = "idAdministrator", referencedColumnName = "idUser") +public class Administrator extends User { + + @OneToMany(mappedBy = "projectAdministrator", fetch = FetchType.LAZY, orphanRemoval = true) + private final List listProject = new ArrayList<>(); + + /*@OneToMany(mappedBy = "administratorSectionCell", fetch = FetchType.LAZY, orphanRemoval = true) + private List listSectionCell = new ArrayList<>();*/ + // should now be useless + + @OneToMany(mappedBy = "administratorAnnotation", fetch = FetchType.LAZY, orphanRemoval = true) + private final List listAnnotation = new ArrayList<>(); + + /*@OneToMany(mappedBy = "administratorAppointment", fetch = FetchType.LAZY, orphanRemoval = true) + private final List listAppointment = new ArrayList<>();*/ + // should now be useless + + @OneToOne(mappedBy = "administratorAppointment", fetch = FetchType.LAZY, orphanRemoval = true) + private MakeAppointment makeAppointment; + + public Administrator() {} + + public Administrator( + String userSurname, + String username, + String primaryMail, + String secondaryMail, + String phoneNumber) { + super(null, userSurname, username, primaryMail, secondaryMail, phoneNumber); + } + + public List getListProject() { + return listProject; + } + + public void updateListProject(Project project) { + listProject.add(project); + } + + public List getListAnnotation() { + return listAnnotation; + } + + public void updateListAnnotation(Annotation annotation) { + listAnnotation.add(annotation); + } + + public MakeAppointment getMakeAppointment() { + return makeAppointment; + } + + public void setMakeAppointment(MakeAppointment makeAppointment) { + this.makeAppointment = makeAppointment; + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/model/Annotation.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/Annotation.java new file mode 100644 index 0000000..f34f663 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/Annotation.java @@ -0,0 +1,61 @@ +package enseirb.myinpulse.model; + +import jakarta.persistence.*; + +@Entity +@Table(name = "annotation") +public class Annotation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long idAnnotation; + + private String comment; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "idSectionCell") + private SectionCell sectionCellAnnotation; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "idAdministrator") + private Administrator administratorAnnotation; + + public Annotation() {} + + public Annotation(Long idAnnotation, String commentary) { + this.idAnnotation = idAnnotation; + this.comment = comment; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public Long getIdAnnotation() { + return idAnnotation; + } + + public void setIdAnnotation(Long idAnnotation) { + this.idAnnotation = idAnnotation; + } + + public SectionCell getSectionCellAnnotation() { + return sectionCellAnnotation; + } + + public void setSectionCellAnnotation(SectionCell sectionCellAnnotation) { + this.sectionCellAnnotation = sectionCellAnnotation; + } + + public Administrator getAdministratorAnnotation() { + return administratorAnnotation; + } + + public void setAdministratorAnnotation(Administrator administratorAnnotation) { + this.administratorAnnotation = administratorAnnotation; + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/model/Appointment.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/Appointment.java new file mode 100644 index 0000000..a683d3f --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/Appointment.java @@ -0,0 +1,126 @@ +package enseirb.myinpulse.model; + +import jakarta.persistence.*; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "appointment") +public class Appointment { + + /*@OneToMany(mappedBy = "appointmentEntrepreneurs", fetch = FetchType.LAZY, orphanRemoval = true) + private final List listEntrepreneur = + new ArrayList<>(); */ + // should now be useless + + @ManyToMany( + fetch = FetchType.LAZY, + cascade = {CascadeType.ALL}) + @JoinTable( + name = "concern", + joinColumns = @JoinColumn(name = "idAppointment"), + inverseJoinColumns = @JoinColumn(name = "idSectionCell")) + List listSectionCell = new ArrayList<>(); + + @OneToOne(mappedBy = "appointmentReport", fetch = FetchType.LAZY, orphanRemoval = true) + private Report report; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long idAppointment; + + private LocalDate appointmentDate; + + private LocalTime appointmentTime; + + private LocalTime appointmentDuration; + + @Column(length = 255) + private String appointmentPlace; + + private String appointmentSubject; + + public Appointment() {} + + public Appointment( + Long idAppointment, + LocalDate appointmentDate, + LocalTime appointmentTime, + LocalTime appointmentDuration, + String appointmentPlace, + String appointmentSubject) { + this.idAppointment = idAppointment; + this.appointmentDate = appointmentDate; + this.appointmentTime = appointmentTime; + this.appointmentDuration = appointmentDuration; + this.appointmentPlace = appointmentPlace; + this.appointmentSubject = appointmentSubject; + } + + public Long getIdAppointment() { + return idAppointment; + } + + public void setIdAppointment(Long idAppointment) { + this.idAppointment = idAppointment; + } + + public LocalDate getAppointmentDate() { + return appointmentDate; + } + + public void setAppointmentDate(LocalDate appointmentDate) { + this.appointmentDate = appointmentDate; + } + + public LocalTime getAppointmentTime() { + return appointmentTime; + } + + public void setAppointmentTime(LocalTime appointmentTime) { + this.appointmentTime = appointmentTime; + } + + public LocalTime getAppointmentDuration() { + return appointmentDuration; + } + + public void setAppointmentDuration(LocalTime appointmentDuration) { + this.appointmentDuration = appointmentDuration; + } + + public String getAppointmentPlace() { + return appointmentPlace; + } + + public void setAppointmentPlace(String appointmentPlace) { + this.appointmentPlace = appointmentPlace; + } + + public String getAppointmentSubject() { + return appointmentSubject; + } + + public void setAppointmentSubject(String appointmentSubject) { + this.appointmentSubject = appointmentSubject; + } + + public List getAppointmentListSectionCell() { + return listSectionCell; + } + + public void updateListSectionCell(SectionCell sectionCell) { + listSectionCell.add(sectionCell); + } + + public Report getAppointmentReport() { + return report; + } + + public void setAppointmentReport(Report report) { + this.report = report; + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/model/Entrepreneur.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/Entrepreneur.java new file mode 100644 index 0000000..35b6e71 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/Entrepreneur.java @@ -0,0 +1,123 @@ +package enseirb.myinpulse.model; + +import jakarta.persistence.*; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +@Entity +@Table(name = "entrepreneur") +@PrimaryKeyJoinColumn(name = "idEntrepreneur", referencedColumnName = "idUser") +public class Entrepreneur extends User { + + @Column(length = 255) + private String school; + + @Column(length = 255) + private String course; + + private boolean sneeStatus; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "idProjectParticipation", referencedColumnName = "idProject") + private Project projectParticipation; + + // @Column(insertable=false, updatable=false) + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "idProjectProposed", referencedColumnName = "idProject") + private Project projectProposed; + + /*@ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "idAppointment") + private Appointment appointmentEntrepreneur;*/ + // should now be useless + + @OneToOne(mappedBy = "entrepreneurAppointment", fetch = FetchType.LAZY, orphanRemoval = true) + private MakeAppointment makeAppointment; + + public Entrepreneur() {} + + public Entrepreneur( + String userSurname, + String username, + String primaryMail, + String secondaryMail, + String phoneNumber, + String school, + String course, + boolean sneeStatus) { + super(userSurname, username, primaryMail, secondaryMail, phoneNumber); + this.school = school; + this.course = course; + this.sneeStatus = sneeStatus; + } + + public Entrepreneur( + Long idUser, + String userSurname, + String userName, + String primaryMail, + String secondaryMail, + String phoneNumber, + String school, + String course, + boolean sneeStatus, + Project projectParticipation, + Project projectProposed, + MakeAppointment makeAppointment) { + super(idUser, userSurname, userName, primaryMail, secondaryMail, phoneNumber); + this.school = school; + this.course = course; + this.sneeStatus = sneeStatus; + this.projectParticipation = projectParticipation; + this.projectProposed = projectProposed; + this.makeAppointment = makeAppointment; + } + + public String getSchool() { + return school; + } + + public void setSchool(String school) { + this.school = school; + } + + public String getCourse() { + return course; + } + + public void setCourse(String course) { + this.course = course; + } + + public boolean isSneeStatus() { + return sneeStatus; + } + + public void setSneeStatus(boolean statusSnee) { + this.sneeStatus = sneeStatus; + } + + public Project getProjectParticipation() { + return projectParticipation; + } + + public void setProjectParticipation(Project projectParticipation) { + this.projectParticipation = projectParticipation; + } + + public Project getProjectProposed() { + return projectProposed; + } + + public void setProjectProposed(Project projectProposed) { + this.projectProposed = projectProposed; + } + + public MakeAppointment getMakeAppointment() { + return makeAppointment; + } + + public void setMakeAppointment(MakeAppointment makeAppointment) { + this.makeAppointment = makeAppointment; + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/model/MakeAppointment.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/MakeAppointment.java new file mode 100644 index 0000000..aae4f18 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/MakeAppointment.java @@ -0,0 +1,26 @@ +package enseirb.myinpulse.model; + +import jakarta.persistence.*; + +@Entity +@Table(name = "make_appointment") +public class MakeAppointment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long idMakeAppointment; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "idAdministrator") + private Administrator administratorAppointment; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "idEntrepreneur") + private Entrepreneur entrepreneurAppointment; + + public MakeAppointment() {} + + public MakeAppointment(Long idMakeAppointment) { + this.idMakeAppointment = idMakeAppointment; + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/model/Project.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/Project.java new file mode 100644 index 0000000..866e685 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/Project.java @@ -0,0 +1,140 @@ +package enseirb.myinpulse.model; + +import jakarta.persistence.*; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "project") +public class Project { + + @OneToMany(mappedBy = "projectParticipation", fetch = FetchType.LAZY, orphanRemoval = true) + private final List listEntrepreneurParticipation = new ArrayList<>(); + + @OneToMany(mappedBy = "projectSectionCell", fetch = FetchType.LAZY, orphanRemoval = true) + private final List listSectionCell = new ArrayList<>(); + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long idProject; + + @Column(length = 255) + private String projectName; + + private byte[] logo; + private LocalDate creationDate; + + @Column private ProjectDecisionValue projectStatus; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "idAdministrator") + private Administrator projectAdministrator; + + @OneToOne(mappedBy = "projectProposed", fetch = FetchType.LAZY, orphanRemoval = true) + private Entrepreneur entrepreneurProposed; + + public Project() {} + + public Project( + String projectName, + byte[] logo, + LocalDate creationDate, + ProjectDecisionValue projectStatus, + Administrator projectAdministrator) { + this.projectName = projectName; + this.logo = logo; + this.creationDate = creationDate; + // this.projectStatus = (long) projectStatus.ordinal(); + this.projectStatus = projectStatus; + this.projectAdministrator = projectAdministrator; + } + + public Project( + String projectName, + byte[] logo, + LocalDate creationDate, + ProjectDecisionValue projectStatus, + Administrator projectAdministrator, + Entrepreneur entrepreneurProposed) { + this.projectName = projectName; + this.logo = logo; + this.creationDate = creationDate; + this.projectStatus = projectStatus; + this.projectAdministrator = projectAdministrator; + this.entrepreneurProposed = entrepreneurProposed; + } + + public Long getIdProject() { + return idProject; + } + + public void setIdProject(Long idProject) { + this.idProject = idProject; + } + + public String getProjectName() { + return projectName; + } + + public void setProjectName(String projectName) { + this.projectName = projectName; + } + + public byte[] getLogo() { + return logo; + } + + public void setLogo(byte[] logo) { + this.logo = logo; + } + + public LocalDate getCreationDate() { + return creationDate; + } + + public void setCreationDate(LocalDate creationDate) { + this.creationDate = creationDate; + } + + public ProjectDecisionValue getProjectStatus() { + return projectStatus; + } + + public void setProjectStatus(ProjectDecisionValue projectStatus) { + this.projectStatus = projectStatus; + } + + public List getListEntrepreneurParticipation() { + return listEntrepreneurParticipation; + } + + public void updateListEntrepreneurParticipation(Entrepreneur projectParticipant) { + listEntrepreneurParticipation.add(projectParticipant); + } + + public List getListSectionCell() { + return listSectionCell; + } + + public void updateListSectionCell(SectionCell projectSectionCell) { + listSectionCell.add(projectSectionCell); + } + + public Administrator getProjectAdministrator() { + return projectAdministrator; + } + + public void setProjectAdministrator(Administrator projectAdministrator) { + this.projectAdministrator = projectAdministrator; + } + + public Entrepreneur getEntrepreneurProposed() { + return entrepreneurProposed; + } + + public void setEntrepreneurProposed(Entrepreneur entrepreneurProposed) { + this.entrepreneurProposed = entrepreneurProposed; + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/model/ProjectDecision.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/ProjectDecision.java new file mode 100644 index 0000000..22eec2d --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/ProjectDecision.java @@ -0,0 +1,25 @@ +package enseirb.myinpulse.model; + +public class ProjectDecision { + public long projectId; + public long adminId; + public long isAccepted; + + public ProjectDecision(long projectId, long adminId, long isAccepted) { + this.projectId = projectId; + this.adminId = adminId; + this.isAccepted = isAccepted; + } + + @Override + public String toString() { + return "ProjectDecision{" + + "projectId=" + + projectId + + ", adminId=" + + adminId + + ", isAccepted=" + + isAccepted + + '}'; + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/model/ProjectDecisionValue.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/ProjectDecisionValue.java new file mode 100644 index 0000000..a47c5f9 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/ProjectDecisionValue.java @@ -0,0 +1,9 @@ +package enseirb.myinpulse.model; + +public enum ProjectDecisionValue { + PENDING, + ACTIVE, + ENDED, + ABORTED, + REJECTED, +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/model/Report.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/Report.java new file mode 100644 index 0000000..40e3337 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/Report.java @@ -0,0 +1,48 @@ +package enseirb.myinpulse.model; + +import jakarta.persistence.*; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "report") +public class Report { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long idReport; + + private String reportContent; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "idAppointment") + private Appointment appointmentReport; + + public Report() {} + + public Report(Long idReport, String reportContent) { + this.idReport = idReport; + this.reportContent = reportContent; + } + + public Long getIdReport() { + return idReport; + } + + public String getReportContent() { + return reportContent; + } + + public void setReportContent(String reportContent) { + this.reportContent = reportContent; + } + + public Appointment getAppointmentReport() { + return appointmentReport; + } + + public void setAppointmentReport(Appointment appointmentReport) { + this.appointmentReport = appointmentReport; + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/model/RoleRepresentation.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/RoleRepresentation.java new file mode 100644 index 0000000..472c25b --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/RoleRepresentation.java @@ -0,0 +1,7 @@ +package enseirb.myinpulse.model; + +public class RoleRepresentation { + public String id; + public String name; + public String description; +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/model/SectionCell.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/SectionCell.java new file mode 100644 index 0000000..2bd1888 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/SectionCell.java @@ -0,0 +1,110 @@ +package enseirb.myinpulse.model; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "section_cell") +public class SectionCell { + + @ManyToMany(mappedBy = "listSectionCell") + private final List listAppointment = new ArrayList<>(); + + @OneToMany(mappedBy = "sectionCellAnnotation", fetch = FetchType.LAZY, orphanRemoval = true) + private final List listAnnotation = new ArrayList<>(); + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long idSectionCell; + + @Column() private long sectionId; + private String contentSectionCell; + + /*@ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "idAdministrator") + private Administrator administratorSectionCell;*/ + // should now be useless + private LocalDateTime modificationDate; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "idProject") + private Project projectSectionCell; + + public SectionCell() {} + + public SectionCell( + Long idSectionCell, + Long sectionId, + String contentSectionCell, + LocalDateTime modificationDate, + Project projectSectionCell) { + this.idSectionCell = idSectionCell; + this.sectionId = sectionId; + this.contentSectionCell = contentSectionCell; + this.modificationDate = modificationDate; + this.projectSectionCell = projectSectionCell; + } + + public Long getIdSectionCell() { + return idSectionCell; + } + + public void setIdSectionCell(Long idSectionCell) { + this.idSectionCell = idSectionCell; + } + + public Long getSectionId() { + return sectionId; + } + + public void setSectionId(Long sectionId) { + this.sectionId = sectionId; + } + + public String getContentSectionCell() { + return contentSectionCell; + } + + public void setContentSectionCell(String contentSectionCell) { + this.contentSectionCell = contentSectionCell; + } + + public LocalDateTime getModificationDate() { + return modificationDate; + } + + public void setModificationDate(LocalDateTime modificationDate) { + this.modificationDate = modificationDate; + } + + public Project getProjectSectionCell() { + return projectSectionCell; + } + + public List getAppointmentSectionCell() { + return listAppointment; + } + + public void updateAppointmentSectionCell(Appointment appointment) { + listAppointment.add(appointment); + } + + public List getListAnnotation() { + return listAnnotation; + } + + public void updateListAnnotation(Annotation annotation) { + listAnnotation.add(annotation); + } + + public void setSectionId(long sectionId) { + this.sectionId = sectionId; + } + + public void setProjectSectionCell(Project projectSectionCell) { + this.projectSectionCell = projectSectionCell; + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/model/User.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/User.java new file mode 100644 index 0000000..37a551d --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/User.java @@ -0,0 +1,108 @@ +package enseirb.myinpulse.model; + +import jakarta.persistence.*; + +@Entity +@Table(name = "user_inpulse") +@Inheritance(strategy = InheritanceType.JOINED) +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long idUser; + + @Column(length = 255) + private String userSurname; + + @Column(length = 255) + private String userName; + + @Column(length = 255) + private String primaryMail; + + @Column(length = 255) + private String secondaryMail; + + @Column(length = 20) + private String phoneNumber; + + public User() {} + + // TODO: this should be removed as we shouldn't be able to chose the ID. Leaving it for + // compatibility purposes, as soon as it's not used anymore, delete it + public User( + Long idUser, + String userSurname, + String userName, + String primaryMail, + String secondaryMail, + String phoneNumber) { + this.idUser = idUser; + this.userSurname = userSurname; + this.userName = userName; + this.primaryMail = primaryMail; + this.secondaryMail = secondaryMail; + this.phoneNumber = phoneNumber; + } + + public User( + String userSurname, + String userName, + String primaryMail, + String secondaryMail, + String phoneNumber) { + this.userSurname = userSurname; + this.userName = userName; + this.primaryMail = primaryMail; + this.secondaryMail = secondaryMail; + this.phoneNumber = phoneNumber; + } + + public Long getIdUser() { + return idUser; + } + + public void setIdUser(Long idUser) { + this.idUser = idUser; + } + + public String getUserSurname() { + return userSurname; + } + + public void setUserSurname(String userSurname) { + userSurname = userSurname; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + userName = userName; + } + + public String getPrimaryMail() { + return primaryMail; + } + + public void setPrimaryMail(String primaryMail) { + this.primaryMail = primaryMail; + } + + public String getSecondaryMail() { + return secondaryMail; + } + + public void setSecondaryMail(String secondaryMail) { + this.secondaryMail = secondaryMail; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public void setPhoneNumber(String phoneNumber) { + phoneNumber = phoneNumber; + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/model/UserRepresentation.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/UserRepresentation.java new file mode 100644 index 0000000..995b417 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/model/UserRepresentation.java @@ -0,0 +1,6 @@ +package enseirb.myinpulse.model; + +public class UserRepresentation { + public String id; + public String name; +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/AdministratorRepository.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/AdministratorRepository.java new file mode 100644 index 0000000..bcf05c7 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/AdministratorRepository.java @@ -0,0 +1,17 @@ +package enseirb.myinpulse.repository; + +import enseirb.myinpulse.model.Administrator; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +import java.util.Optional; + +@RepositoryRestResource +public interface AdministratorRepository extends JpaRepository { + + /* @Query("SELECT a from Administrators a") + Administrator findAllAdministrator(); */ + + Optional findByPrimaryMail(String PrimaryMail); +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/AnnotationRepository.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/AnnotationRepository.java new file mode 100644 index 0000000..8be8adc --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/AnnotationRepository.java @@ -0,0 +1,9 @@ +package enseirb.myinpulse.repository; + +import enseirb.myinpulse.model.Annotation; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +@RepositoryRestResource +public interface AnnotationRepository extends JpaRepository {} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/AppointmentRepository.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/AppointmentRepository.java new file mode 100644 index 0000000..11b0c00 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/AppointmentRepository.java @@ -0,0 +1,9 @@ +package enseirb.myinpulse.repository; + +import enseirb.myinpulse.model.Appointment; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +@RepositoryRestResource +public interface AppointmentRepository extends JpaRepository {} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/EntrepreneurRepository.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/EntrepreneurRepository.java new file mode 100644 index 0000000..15a2222 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/EntrepreneurRepository.java @@ -0,0 +1,17 @@ +package enseirb.myinpulse.repository; + +import enseirb.myinpulse.model.Entrepreneur; +import enseirb.myinpulse.model.Project; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +@RepositoryRestResource +public interface EntrepreneurRepository extends JpaRepository { + + Iterable getEntrepreneurByProjectParticipation(Project project); + + /* @Query("SELECT e from Entrepreneur e") + Entrepreneur findAllEntrepreneurl(); */ + +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/MakeAppointmentRepository.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/MakeAppointmentRepository.java new file mode 100644 index 0000000..c49ef05 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/MakeAppointmentRepository.java @@ -0,0 +1,9 @@ +package enseirb.myinpulse.repository; + +import enseirb.myinpulse.model.MakeAppointment; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +@RepositoryRestResource +public interface MakeAppointmentRepository extends JpaRepository {} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/ProjectRepository.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/ProjectRepository.java new file mode 100644 index 0000000..7cf5214 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/ProjectRepository.java @@ -0,0 +1,19 @@ +package enseirb.myinpulse.repository; + +import enseirb.myinpulse.model.Administrator; +import enseirb.myinpulse.model.Project; +import enseirb.myinpulse.model.ProjectDecisionValue; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +import java.util.Optional; + +@RepositoryRestResource +public interface ProjectRepository extends JpaRepository { + Iterable findByProjectAdministrator(Administrator administrator); + + Iterable findByProjectStatus(ProjectDecisionValue status); + + Optional findByProjectName(String projectName); +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/ReportRepository.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/ReportRepository.java new file mode 100644 index 0000000..cc70746 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/ReportRepository.java @@ -0,0 +1,9 @@ +package enseirb.myinpulse.repository; + +import enseirb.myinpulse.model.Report; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +@RepositoryRestResource +public interface ReportRepository extends JpaRepository {} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/SectionCellRepository.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/SectionCellRepository.java new file mode 100644 index 0000000..4ad5b51 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/SectionCellRepository.java @@ -0,0 +1,18 @@ +package enseirb.myinpulse.repository; + +import enseirb.myinpulse.model.Project; +import enseirb.myinpulse.model.SectionCell; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +import java.time.LocalDateTime; + +@RepositoryRestResource +public interface SectionCellRepository extends JpaRepository { + + Iterable findByProjectSectionCellAndSectionId(Project project, long sectionId); + + Iterable findByProjectSectionCellAndSectionIdAndModificationDateBefore( + Project project, long sectionId, LocalDateTime date); +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/UserRepository.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/UserRepository.java new file mode 100644 index 0000000..291a97d --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/repository/UserRepository.java @@ -0,0 +1,17 @@ +package enseirb.myinpulse.repository; + +import enseirb.myinpulse.model.User; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +import java.util.Optional; + +@RepositoryRestResource +public interface UserRepository extends JpaRepository { + Optional findByPrimaryMail(String email); + + /* @Query("SELECT u from User u") + User findAllUser(); */ + +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/security/KeycloakJwtRolesConverter.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/security/KeycloakJwtRolesConverter.java deleted file mode 100644 index fafbef5..0000000 --- a/MyINPulse-back/src/main/java/enseirb/myinpulse/security/KeycloakJwtRolesConverter.java +++ /dev/null @@ -1,98 +0,0 @@ -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/java/enseirb/myinpulse/service/AdminApiService.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/AdminApiService.java new file mode 100644 index 0000000..4fd1ccc --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/AdminApiService.java @@ -0,0 +1,166 @@ +package enseirb.myinpulse.service; + +import static enseirb.myinpulse.model.ProjectDecisionValue.ACTIVE; +import static enseirb.myinpulse.model.ProjectDecisionValue.REJECTED; + +import enseirb.myinpulse.model.*; +import enseirb.myinpulse.service.database.*; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class AdminApiService { + + protected static final Logger logger = LogManager.getLogger(); + + private final ProjectService projectService; + private final UserService userService; + private final AdministratorService administratorService; + private final UtilsService utilsService; + private final AppointmentService appointmentService; + private final ReportService reportService; + private final SectionCellService sectionCellService; + + @Autowired + AdminApiService( + ProjectService projectService, + UserService userService, + AdministratorService administratorService, + UtilsService utilsService, + AppointmentService appointmentService, + ReportService reportService, + SectionCellService sectionCellService) { + this.projectService = projectService; + this.userService = userService; + this.administratorService = administratorService; + this.utilsService = utilsService; + this.appointmentService = appointmentService; + this.reportService = reportService; + this.sectionCellService = sectionCellService; + } + + // TODO: check if tests are sufficient - peer verification required + public Iterable getProjectsOfAdmin(String mail) { + return projectService.getProjectsByAdminId( + administratorService.getAdministratorById( + this.userService.getUserByEmail(mail).getIdUser())); + } + + public Iterable getUpcomingAppointments(String mail) { + logger.info("User {} check their upcoming appointments", mail); + User user = this.userService.getUserByEmail(mail); + List appointments = new ArrayList<>(); + if (user instanceof Administrator) { + List projects = new ArrayList<>(((Administrator) user).getListProject()); + projects.forEach( + project -> { + project.getListSectionCell() + .forEach( + sectionCell -> { + appointments.addAll( + this.sectionCellService + .getAppointmentsBySectionCellId( + sectionCell + .getIdSectionCell())); + }); + }); + } + if (user instanceof Entrepreneur) { + Project project = ((Entrepreneur) user).getProjectParticipation(); + project.getListSectionCell() + .forEach( + sectionCell -> { + appointments.addAll( + this.sectionCellService.getAppointmentsBySectionCellId( + sectionCell.getIdSectionCell())); + }); + } + return appointments; + } + + // TODO: check if tests are sufficient - peer verification required + public Iterable getPendingProjects() { + return this.projectService.getPendingProjects(); + } + + // TODO: check if tests are sufficient - peer verification required + public void validateProject(ProjectDecision decision) { + projectService.updateProject( + decision.projectId, + null, + null, + null, + (decision.isAccepted == 1) ? ACTIVE : REJECTED, + this.administratorService.getAdministratorById(decision.adminId)); + } + + // TODO: check if tests are sufficient - peer verification required + public void addNewProject(Project project) { + project.setIdProject(null); + // We remove the ID from the request to be sure that it will be auto generated + try { + this.projectService.getProjectByName(project.getProjectName(), true); + throw new ResponseStatusException(HttpStatus.CONFLICT, "Project already exists"); + } catch (ResponseStatusException e) { + if (e.getStatusCode() == HttpStatus.CONFLICT) { + throw new ResponseStatusException(HttpStatus.CONFLICT, "Project already exists"); + } + } + Project newProject = projectService.addNewProject(project); + if (project.getProjectAdministrator() != null) { + newProject.getProjectAdministrator().updateListProject(newProject); + } + if (newProject.getEntrepreneurProposed() != null) { + Entrepreneur proposed = newProject.getEntrepreneurProposed(); + proposed.setProjectProposed(newProject); + proposed.setProjectParticipation(newProject); + } + newProject + .getListEntrepreneurParticipation() + .forEach( + participation -> { + participation.setProjectParticipation(newProject); + }); + newProject + .getListSectionCell() + .forEach( + sectionCell -> { + sectionCell.setProjectSectionCell(newProject); + }); + } + + public void createAppointmentReport(long appointmentId, Report report, String mail) { + long projectId = + this.appointmentService + .getAppointmentById(appointmentId) + .getAppointmentListSectionCell() + .getFirst() + .getProjectSectionCell() + .getIdProject(); + if (!utilsService.isAllowedToCheckProject(mail, projectId)) { + logger.warn( + "User {} tried to add an report for appointment {} but is not allowed to.", + mail, + projectId); + throw new ResponseStatusException( + HttpStatus.UNAUTHORIZED, "You're not allowed to check this project"); + } + logger.info("User {} added a report for appointment {}", mail, projectId); + Report addedReport = this.reportService.addNewReport(report); + addedReport.setAppointmentReport(this.appointmentService.getAppointmentById(appointmentId)); + this.appointmentService.getAppointmentById(appointmentId).setAppointmentReport(addedReport); + } + + // TODO: test + public void deleteProject(long projectId) { + this.projectService.deleteProjectById(projectId); + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/service/EntrepreneurApiService.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/EntrepreneurApiService.java new file mode 100644 index 0000000..1e9cd92 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/EntrepreneurApiService.java @@ -0,0 +1,135 @@ +package enseirb.myinpulse.service; + +import static enseirb.myinpulse.model.ProjectDecisionValue.PENDING; + +import enseirb.myinpulse.model.Project; +import enseirb.myinpulse.model.SectionCell; +import enseirb.myinpulse.service.database.ProjectService; +import enseirb.myinpulse.service.database.SectionCellService; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +@Service +public class EntrepreneurApiService { + + protected static final Logger logger = LogManager.getLogger(); + + private final SectionCellService sectionCellService; + private final ProjectService projectService; + private final UtilsService utilsService; + + @Autowired + EntrepreneurApiService( + SectionCellService sectionCellService, + ProjectService projectService, + UtilsService utilsService) { + this.sectionCellService = sectionCellService; + this.projectService = projectService; + this.utilsService = utilsService; + } + + public void editSectionCell(Long sectionCellId, SectionCell sectionCell, String mail) { + SectionCell editSectionCell = sectionCellService.getSectionCellById(sectionCellId); + if (editSectionCell == null) { + System.err.println("Trying to edit unknown section cell"); + throw new ResponseStatusException( + HttpStatus.NOT_FOUND, "Cette cellule de section n'existe pas"); + } + if (!utilsService.isAllowedToCheckProject( + mail, this.sectionCellService.getProjectId(sectionCellId))) { + logger.warn( + "User {} tried to edit section cells {} of the project {} but is not allowed to.", + mail, + sectionCellId, + this.sectionCellService.getProjectId(sectionCellId)); + throw new ResponseStatusException( + HttpStatus.UNAUTHORIZED, "You're not allowed to check this project"); + } + logger.info( + "User {} edited section cell {} of the project with id {}", + mail, + sectionCellId, + this.sectionCellService.getProjectId(sectionCellId)); + sectionCellService.updateSectionCell( + sectionCellId, + sectionCell.getSectionId(), + sectionCell.getContentSectionCell(), + sectionCell.getModificationDate()); + } + + public void removeSectionCell(Long sectionCellId, String mail) { + SectionCell editSectionCell = sectionCellService.getSectionCellById(sectionCellId); + if (editSectionCell == null) { + System.err.println("Trying to remove unknown section cell"); + throw new ResponseStatusException( + HttpStatus.NOT_FOUND, "Cette cellule de section n'existe pas"); + } + if (!utilsService.isAllowedToCheckProject( + mail, this.sectionCellService.getProjectId(sectionCellId))) { + logger.warn( + "User {} tried to remove section cells {} of the project {} but is not allowed to.", + mail, + sectionCellId, + this.sectionCellService.getSectionCellById(sectionCellId)); + throw new ResponseStatusException( + HttpStatus.UNAUTHORIZED, "You're not allowed to check this project"); + } + logger.info( + "User {} removed section cell {} of the project with id {}", + mail, + sectionCellId, + this.sectionCellService.getProjectId(sectionCellId)); + sectionCellService.removeSectionCellById(sectionCellId); + } + + public void addSectionCell(SectionCell sectionCell, String mail) { + if (sectionCell == null) { + System.err.println("Trying to create an empty section cell"); + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, "La cellule de section fournie est vide"); + } + if (!utilsService.isAllowedToCheckProject( + mail, this.sectionCellService.getProjectId(sectionCell.getIdSectionCell()))) { + logger.warn( + "User {} tried to add a section cell to the project {} but is not allowed to.", + mail, + this.sectionCellService.getProjectId(sectionCell.getIdSectionCell())); + throw new ResponseStatusException( + HttpStatus.UNAUTHORIZED, "You're not allowed to check this project"); + } + logger.info( + "User {} added a new section cell {} to the project with id {}", + mail, + sectionCell.getIdSectionCell(), + this.sectionCellService.getProjectId(sectionCell.getIdSectionCell())); + SectionCell newSectionCell = sectionCellService.addNewSectionCell(sectionCell); + newSectionCell.getProjectSectionCell().updateListSectionCell(newSectionCell); + newSectionCell + .getAppointmentSectionCell() + .forEach( + appointment -> { + appointment.updateListSectionCell(newSectionCell); + }); + newSectionCell + .getListAnnotation() + .forEach( + annotation -> { + annotation.setSectionCellAnnotation(newSectionCell); + }); + } + + public void requestNewProject(Project project, String mail) { + if (project == null) { + logger.error("Trying to request the creation of a null project"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Le projet fourni est vide"); + } + logger.info("User {} created a new project with id {}", mail, project.getIdProject()); + project.setProjectStatus(PENDING); + projectService.addNewProject(project); + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/service/KeycloakApi.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/KeycloakApi.java new file mode 100644 index 0000000..fb97c70 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/KeycloakApi.java @@ -0,0 +1,135 @@ +package enseirb.myinpulse.service; + +import static org.springframework.http.MediaType.APPLICATION_JSON; + +import enseirb.myinpulse.exception.UserNotFoundException; +import enseirb.myinpulse.model.RoleRepresentation; +import enseirb.myinpulse.model.UserRepresentation; + +import org.springframework.web.client.RestClient; + +import javax.management.relation.RoleNotFoundException; + +public class KeycloakApi { + + static final String keycloakUrl; + static final String realmName; + + static { + if (System.getenv("VITE_KEYCLOAK_URL") == null) { + System.exit(-1); + } + keycloakUrl = System.getenv("VITE_KEYCLOAK_URL"); + } + + static { + if (System.getenv("VITE_KEYCLOAK_REALM") == null) { + System.exit(-1); + } + realmName = System.getenv("VITE_KEYCLOAK_REALM"); + } + + /** + * Uses Keycloak API to retrieve a role representation of a role by its name + * + * @param roleName name of the role + * @param bearer authorization header used by the client to authenticate to keycloak + */ + public static RoleRepresentation getRoleRepresentationByName(String roleName, String bearer) + throws RoleNotFoundException { + RoleRepresentation[] response = + RestClient.builder() + .baseUrl(keycloakUrl) + .defaultHeader("Authorization", bearer) + .build() + .get() + .uri("/admin/realms/{realmName}/roles/{roleName}", realmName, roleName) + .retrieve() + .body(RoleRepresentation[].class); + + if (response == null || response.length == 0) { + throw new RoleNotFoundException("Role not found"); + } + return response[0]; + } + + /** + * Use keycloak API to to retreive a userID via his name or email. + * + * @param username username or mail of the user + * @param bearer bearer of the user, allowing access to database + * @return the userid, as a String + * @throws UserNotFoundException + */ + public static String getUserIdByName(String username, String bearer) + throws UserNotFoundException { + UserRepresentation[] response = + RestClient.builder() + .baseUrl(keycloakUrl) + .defaultHeader("Authorization", bearer) + .build() + .get() + .uri( + "/admin/realms/{realmName}/users?username={username}", + realmName, + username) + .retrieve() + .body(UserRepresentation[].class); + + if (response == null || response.length == 0) { + throw new UserNotFoundException("User not found"); + } + return response[0].id; + } + + /** + * TODO: check for error + * + *

Set a keycloak role to a keycloak user. + * + *

Usual roles should be `MyINPulse-admin` and `MyINPulse-entrepreneur` + * + * @param username + * @param roleName + * @param bearer + * @throws RoleNotFoundException + * @throws UserNotFoundException + */ + public static void setRoleToUser(String username, String roleName, String bearer) + throws RoleNotFoundException, UserNotFoundException { + RoleRepresentation roleRepresentation = getRoleRepresentationByName(roleName, bearer); + String userId = getUserIdByName(username, bearer); + + RestClient.builder() + .baseUrl(keycloakUrl) + .defaultHeader("Authorization", bearer) + .build() + .post() + .uri( + "/admin/realms/${realmName}/users/${userId}/role-mappings/realm", + realmName, + userId) + .body(roleRepresentation) + .contentType(APPLICATION_JSON) + .retrieve(); + } + + /** + * Delete a user from Keycloak database. TODO: check the bearer permission. + * + * @param username + * @param bearer + * @throws UserNotFoundException + */ + public static void deleteUser(String username, String bearer) throws UserNotFoundException { + String userId = getUserIdByName(username, bearer); + + RestClient.builder() + .baseUrl(keycloakUrl) + .defaultHeader("Authorization", bearer) + .build() + .delete() + .uri("/admin/realms/${realmName}/users/${userId}", realmName, userId) + .retrieve(); + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/service/SharedApiService.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/SharedApiService.java new file mode 100644 index 0000000..23ad616 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/SharedApiService.java @@ -0,0 +1,252 @@ +package enseirb.myinpulse.service; + +import com.itextpdf.text.*; +import com.itextpdf.text.pdf.PdfWriter; + +import enseirb.myinpulse.model.*; +import enseirb.myinpulse.service.database.*; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +@Service +public class SharedApiService { + + protected static final Logger logger = LogManager.getLogger(); + + private final ProjectService projectService; + private final EntrepreneurService entrepreneurService; + private final SectionCellService sectionCellService; + private final AppointmentService appointmentService; + + private final UtilsService utilsService; + + @Autowired + SharedApiService( + ProjectService projectService, + EntrepreneurService entrepreneurService, + SectionCellService sectionCellService, + AppointmentService appointmentService, + UtilsService utilsService) { + this.projectService = projectService; + this.entrepreneurService = entrepreneurService; + this.sectionCellService = sectionCellService; + this.appointmentService = appointmentService; + this.utilsService = utilsService; + } + + // TODO filter this with date + public Iterable getSectionCells( + long projectId, long sectionId, String date, String mail) { + + if (!utilsService.isAllowedToCheckProject(mail, projectId)) { + logger.warn( + "User {} tried to check section cells of the project {} but is not allowed to.", + mail, + projectId); + throw new ResponseStatusException( + HttpStatus.UNAUTHORIZED, "You're not allowed to check this project"); + } + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + LocalDateTime dateTime = LocalDateTime.parse(date, formatter); + + Project project = this.projectService.getProjectById(projectId); + return this.sectionCellService.getSectionCellsByProjectAndSectionIdBeforeDate( + project, sectionId, dateTime); + } + + // TODO: test + public Iterable getEntrepreneursByProjectId(long projectId, String mail) { + if (!utilsService.isAllowedToCheckProject(mail, projectId)) { + logger.warn( + "User {} tried to check the member of the project {} but is not allowed to.", + mail, + projectId); + throw new ResponseStatusException( + HttpStatus.UNAUTHORIZED, "You're not allowed to check this project"); + } + Project project = this.projectService.getProjectById(projectId); + return this.entrepreneurService.GetEntrepreneurByProject(project); + } + + // TODO: test + public Administrator getAdminByProjectId(long projectId, String mail) { + if (!utilsService.isAllowedToCheckProject(mail, projectId)) { + logger.warn( + "User {} tried to check the admin of the project {} but is not allowed to.", + mail, + projectId); + throw new ResponseStatusException( + HttpStatus.UNAUTHORIZED, "You're not allowed to check this project"); + } + Project project = this.projectService.getProjectById(projectId); + return project.getProjectAdministrator(); + } + + public Iterable getAppointmentsByProjectId(long projectId, String mail) { + if (!utilsService.isAllowedToCheckProject(mail, projectId)) { + logger.warn( + "User {} tried to check the appointments related to the project {} but is not allowed to.", + mail, + projectId); + throw new ResponseStatusException( + HttpStatus.UNAUTHORIZED, "You're not allowed to check this project"); + } + logger.info( + "User {} tried to check the appointments related to the project {}", + mail, + projectId); + Iterable sectionCells = + this.sectionCellService.getSectionCellsByProject( + projectService.getProjectById(projectId), + 2L); // sectionId useless in this function ? + List appointments = new ArrayList(); + sectionCells.forEach( + sectionCell -> { + appointments.addAll( + this.sectionCellService.getAppointmentsBySectionCellId( + sectionCell.getIdSectionCell())); + }); + return appointments; + } + + public void getPDFReport(long appointmentId, String mail) + throws DocumentException, URISyntaxException, IOException { + long projectId = + this.appointmentService + .getAppointmentById(appointmentId) + .getAppointmentListSectionCell() + .getFirst() + .getProjectSectionCell() + .getIdProject(); + if (!utilsService.isAllowedToCheckProject(mail, projectId)) { + logger.warn( + "User {} tried to generate the PDF report {} related to the appointment {} but is not allowed to.", + mail, + this.appointmentService + .getAppointmentById(appointmentId) + .getAppointmentReport() + .getIdReport(), + appointmentId); + throw new ResponseStatusException( + HttpStatus.UNAUTHORIZED, "You're not allowed to check this project"); + } + logger.info( + "User {} generated the PDF report related to appointment {}", mail, appointmentId); + + String reportContent = + this.appointmentService + .getAppointmentById(appointmentId) + .getAppointmentReport() + .getReportContent(); + + // PDF generation + Document document = new Document(); + PdfWriter.getInstance(document, new FileOutputStream("Report" + appointmentId + ".pdf")); + document.open(); + + Paragraph title = + new Paragraph( + new Phrase( + "Compte Rendu - Réunion du " + + this.appointmentService + .getAppointmentById(appointmentId) + .getAppointmentDate() + .toString(), + FontFactory.getFont( + FontFactory.HELVETICA, + 20, + Font.BOLDITALIC, + BaseColor.BLACK))); + title.setAlignment(Element.ALIGN_CENTER); + document.add(title); + + Font subsection = + FontFactory.getFont(FontFactory.HELVETICA, 14, Font.UNDERLINE, BaseColor.DARK_GRAY); + Font body = FontFactory.getFont(FontFactory.COURIER, 12, BaseColor.BLACK); + + String[] split = reportContent.split(" "); + + String tmp = ""; + int counter = 1; + for (String s : split) { + if (s.equals("//")) { + Chunk chunk = new Chunk(tmp, body); + document.add(chunk); + document.add(new Paragraph("\n")); + tmp = ""; + Paragraph paragraph = new Paragraph("Point n°" + counter + " : ", subsection); + document.add(paragraph); + document.add(new Paragraph("\n")); + counter++; + } else { + tmp = tmp.concat(s + " "); + } + } + Chunk chunk = new Chunk(tmp, body); + document.add(chunk); + document.add(new Paragraph("\n")); + + document.close(); + + // Replace uri with website address + Files.copy( + new URI( + "http://localhost:8080/shared/projects/appointments/report/" + + appointmentId) + .toURL() + .openStream(), + Paths.get("Report" + appointmentId + ".pdf"), + StandardCopyOption.REPLACE_EXISTING); + + // delete file, we don't want to stock all reports on the server + File file = new File("Report" + appointmentId + ".pdf"); + if (!file.delete()) { + logger.warn("Failed to delete report {}", file.getAbsolutePath()); + } + } + + public void createAppointmentRequest(Appointment appointment, String mail) { + long projectId = + appointment + .getAppointmentListSectionCell() + .getFirst() + .getProjectSectionCell() + .getIdProject(); + if (!utilsService.isAllowedToCheckProject(mail, projectId)) { + logger.warn( + "User {} tried to create for the project {} but is not allowed to.", + mail, + projectId); + throw new ResponseStatusException( + HttpStatus.UNAUTHORIZED, "You're not allowed to check this project"); + } + logger.info("User {} tried to create an appointment for project {}", mail, projectId); + Appointment newAppointment = this.appointmentService.addNewAppointment(appointment); + newAppointment + .getAppointmentListSectionCell() + .forEach( + sectionCell -> { + sectionCell.updateAppointmentSectionCell(newAppointment); + }); + newAppointment.getAppointmentReport().setAppointmentReport(newAppointment); + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/service/UtilsService.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/UtilsService.java new file mode 100644 index 0000000..a49e82e --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/UtilsService.java @@ -0,0 +1,62 @@ +package enseirb.myinpulse.service; + +import enseirb.myinpulse.model.Administrator; +import enseirb.myinpulse.model.Entrepreneur; +import enseirb.myinpulse.model.Project; +import enseirb.myinpulse.model.User; +import enseirb.myinpulse.service.database.AdministratorService; +import enseirb.myinpulse.service.database.EntrepreneurService; +import enseirb.myinpulse.service.database.ProjectService; +import enseirb.myinpulse.service.database.UserService; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +@Service +public class UtilsService { + + protected static final Logger logger = LogManager.getLogger(); + + private final UserService userService; + private final ProjectService projectService; + private final EntrepreneurService entrepreneurService; + private final AdministratorService administratorService; + + @Autowired + UtilsService( + ProjectService projectService, + UserService userService, + EntrepreneurService entrepreneurService, + AdministratorService administratorService) { + this.userService = userService; + this.projectService = projectService; + this.entrepreneurService = entrepreneurService; + this.administratorService = administratorService; + } + + // TODO: test? + public Boolean isAllowedToCheckProject(String mail, long projectId) { + if (isAnAdmin(mail)) { + return true; + } + User user = this.userService.getUserByEmail(mail); + Entrepreneur entrepreneur = this.entrepreneurService.getEntrepreneurById(user.getIdUser()); + Project project = this.projectService.getProjectById(projectId); + return entrepreneur.getProjectParticipation() == project; + } + + // TODO: test + Boolean isAnAdmin(String mail) { + try { + long userId = this.userService.getUserByEmail(mail).getIdUser(); + Administrator a = this.administratorService.getAdministratorById(userId); + return true; + } catch (ResponseStatusException e) { + logger.info(e); + return false; + } + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/AdministratorService.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/AdministratorService.java new file mode 100644 index 0000000..b3d91bc --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/AdministratorService.java @@ -0,0 +1,60 @@ +package enseirb.myinpulse.service.database; + +import enseirb.myinpulse.model.Administrator; +import enseirb.myinpulse.repository.AdministratorRepository; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Optional; + +@Service +public class AdministratorService { + protected static final Logger logger = LogManager.getLogger(); + + private final AdministratorRepository administratorRepository; + + @Autowired + AdministratorService(AdministratorRepository administratorRepository) { + this.administratorRepository = administratorRepository; + } + + public Iterable allAdministrators() { + return this.administratorRepository.findAll(); + } + + public Administrator getAdministratorById(long id) { + Optional administrator = this.administratorRepository.findById(id); + if (administrator.isEmpty()) { + logger.error("No administrator found with id {}", id); + throw new ResponseStatusException( + HttpStatus.NOT_FOUND, "Cet administrateur n'existe pas"); + } + return administrator.get(); + } + + public Administrator getAdministratorByPrimaryMain(String primaryMail) { + Optional administrator = + this.administratorRepository.findByPrimaryMail(primaryMail); + if (administrator.isEmpty()) { + logger.error("No administrator found with the mail {}", primaryMail); + throw new ResponseStatusException( + HttpStatus.NOT_FOUND, "Cet administrateur n'existe pas"); + } + return administrator.get(); + } + + public Administrator addAdministrator(Administrator administrator) { + return this.administratorRepository.save(administrator); + } + + /* + public Administrator getAdministratorByProject(Project project) { + r + } + */ +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/AnnotationService.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/AnnotationService.java new file mode 100644 index 0000000..577cf9b --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/AnnotationService.java @@ -0,0 +1,61 @@ +package enseirb.myinpulse.service.database; + +import enseirb.myinpulse.model.Annotation; +import enseirb.myinpulse.repository.AnnotationRepository; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Optional; + +@Service +public class AnnotationService { + + protected static final Logger logger = LogManager.getLogger(); + + private final AnnotationRepository annotationRepository; + + @Autowired + AnnotationService(AnnotationRepository annotationRepository) { + this.annotationRepository = annotationRepository; + } + + public Iterable getAllAnnotations() { + return annotationRepository.findAll(); + } + + public Annotation getAnnotationById(Long id) { + Optional annotation = annotationRepository.findById(id); + if (annotation.isEmpty()) { + logger.error("getAnnotationById : No annotation found with id {}", id); + throw new ResponseStatusException( + HttpStatus.NOT_FOUND, "Cette annotation n'existe pas"); + } + return annotation.get(); + } + + public Annotation addNewAnnotation(Annotation annotation) { + return this.annotationRepository.save(annotation); + } + + public void deleteAnnotationById(Long id) { + this.annotationRepository.deleteById(id); + } + + public Annotation updateAnnotation(Long id, String comment) { + Optional annotation = annotationRepository.findById(id); + if (annotation.isEmpty()) { + logger.error("updateAnnotation : No annotation found with id {}", id); + throw new ResponseStatusException( + HttpStatus.NOT_FOUND, "Cette annotation n'existe pas"); + } + if (comment != null) { + annotation.get().setComment(comment); + } + return this.annotationRepository.save(annotation.get()); + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/AppointmentService.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/AppointmentService.java new file mode 100644 index 0000000..7ba0ff5 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/AppointmentService.java @@ -0,0 +1,79 @@ +package enseirb.myinpulse.service.database; + +import enseirb.myinpulse.model.Appointment; +import enseirb.myinpulse.repository.AppointmentRepository; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Optional; + +@Service +public class AppointmentService { + + private static final Logger logger = LogManager.getLogger(AppointmentService.class); + + private AppointmentRepository appointmentRepository; + + @Autowired + AppointmentService(AppointmentRepository appointmentRepository) { + this.appointmentRepository = appointmentRepository; + } + + public Iterable getallAppointments() { + return this.appointmentRepository.findAll(); + } + + public Appointment getAppointmentById(Long id) { + Optional appointment = this.appointmentRepository.findById(id); + if (appointment.isEmpty()) { + logger.error("getAppointmentById : No appointment found with id {}", id); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ce rendez vous n'existe pas"); + } + return appointment.get(); + } + + public Appointment addNewAppointment(Appointment appointment) { + return this.appointmentRepository.save(appointment); + } + + public void deleteAppointmentById(Long id) { + this.appointmentRepository.deleteById(id); + } + + public Appointment updateAppointment( + Long id, + LocalDate appointmentDate, + LocalTime appointmentTime, + LocalTime appointmentDuration, + String appointmentPlace, + String appointmentSubject) { + Optional appointment = this.appointmentRepository.findById(id); + if (appointment.isEmpty()) { + logger.error("updateAppointment : No appointment found with id {}", id); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ce rendez vous n'existe pas"); + } + if (appointmentDate != null) { + appointment.get().setAppointmentDate(appointmentDate); + } + if (appointmentTime != null) { + appointment.get().setAppointmentTime(appointmentTime); + } + if (appointmentDuration != null) { + appointment.get().setAppointmentDuration(appointmentDuration); + } + if (appointmentPlace != null) { + appointment.get().setAppointmentPlace(appointmentPlace); + } + if (appointmentSubject != null) { + appointment.get().setAppointmentSubject(appointmentSubject); + } + return this.appointmentRepository.save(appointment.get()); + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/EntrepreneurService.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/EntrepreneurService.java new file mode 100644 index 0000000..f24878f --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/EntrepreneurService.java @@ -0,0 +1,67 @@ +package enseirb.myinpulse.service.database; + +import enseirb.myinpulse.model.Entrepreneur; +import enseirb.myinpulse.model.Project; +import enseirb.myinpulse.repository.EntrepreneurRepository; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Optional; + +@Service +public class EntrepreneurService { + + protected static final Logger logger = LogManager.getLogger(); + + private final EntrepreneurRepository entrepreneurRepository; + + EntrepreneurService(EntrepreneurRepository entrepreneurRepository) { + this.entrepreneurRepository = entrepreneurRepository; + } + + public Iterable getAllEntrepreneurs() { + return this.entrepreneurRepository.findAll(); + } + + public Entrepreneur getEntrepreneurById(Long id) { + Optional entrepreneur = entrepreneurRepository.findById(id); + if (entrepreneur.isEmpty()) { + logger.error("getEntrepreneurById : No entrepreneur found with id {}", id); + throw new ResponseStatusException( + HttpStatus.NOT_FOUND, "Cet entrepreneur n'existe pas"); + } + return entrepreneur.get(); + } + + public Entrepreneur addEntrepreneur(Entrepreneur entrepreneur) { + return this.entrepreneurRepository.save(entrepreneur); + } + + public Entrepreneur updateEntrepreneur( + Long id, String school, String course, Boolean sneeStatus) { + Optional entrepreneur = entrepreneurRepository.findById(id); + if (entrepreneur.isEmpty()) { + logger.error("updateEntrepreneur : No entrepreneur found with id {}", id); + throw new ResponseStatusException( + HttpStatus.NOT_FOUND, "Cet entrepreneur n'existe pas"); + } + if (school != null) { + entrepreneur.get().setSchool(school); + } + if (course != null) { + entrepreneur.get().setCourse(course); + } + if (sneeStatus != null) { + entrepreneur.get().setSneeStatus(sneeStatus); + } + return this.entrepreneurRepository.save(entrepreneur.get()); + } + + public Iterable GetEntrepreneurByProject(Project project) { + return this.entrepreneurRepository.getEntrepreneurByProjectParticipation(project); + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/ProjectService.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/ProjectService.java new file mode 100644 index 0000000..7eb0651 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/ProjectService.java @@ -0,0 +1,124 @@ +package enseirb.myinpulse.service.database; + +import static enseirb.myinpulse.model.ProjectDecisionValue.PENDING; + +import enseirb.myinpulse.model.Administrator; +import enseirb.myinpulse.model.Project; +import enseirb.myinpulse.model.ProjectDecisionValue; +import enseirb.myinpulse.repository.ProjectRepository; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +@Service +public class ProjectService { + + protected static final Logger logger = LogManager.getLogger(); + + private final ProjectRepository projectRepository; + + @Autowired + ProjectService(ProjectRepository projectRepository) { + this.projectRepository = projectRepository; + } + + public Iterable getAllProjects() { + return this.projectRepository.findAll(); + } + + public Project getProjectById(Long id) { + Optional project = this.projectRepository.findById(id); + if (project.isEmpty()) { + logger.error("No project found with id {}", id); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ce projet n'existe pas"); + } + return project.get(); + } + + public Iterable getProjectsByAdminId(Administrator administrator) { + return this.projectRepository.findByProjectAdministrator(administrator); + } + + // TODO: validation + public Project addNewProject(Project project) { + return this.projectRepository.save(project); + } + + public Project updateProject( + Long id, + String projectName, + byte[] logo, + LocalDate creationDate, + ProjectDecisionValue projectStatus, + Administrator administrator) { + Optional project = this.projectRepository.findById(id); + + if (project.isEmpty()) { + logger.error("Project with id {} not found.", id); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ce projet n'existe pas"); + } + + if (projectName != null) { + project.get().setProjectName(projectName); + } + + if (logo != null) { + project.get().setLogo(logo); + } + + if (creationDate != null) { + project.get().setCreationDate(creationDate); + } + + if (projectStatus != null) { + // TODO: check if this is really useful + /* + if (!validateStatus(projectStatus)) { + logger.error("updateProjectStatus: Invalid status {}", projectStatus); + throw new ResponseStatusException( + HttpStatus.NOT_ACCEPTABLE, "Ce status n'est pas accepté"); + } + */ + project.get().setProjectStatus(projectStatus); + } + + if (administrator != null) { + project.get().setProjectAdministrator(administrator); + } + + return this.projectRepository.save(project.get()); + } + + public Boolean validateStatus(String status) { + return List.of("PENDING", "ACTIVE", "ENDED").contains(status); + } + + public Iterable getPendingProjects() { + return this.projectRepository.findByProjectStatus(PENDING); + } + + public void deleteProjectById(Long id) { + this.projectRepository.deleteById(id); + } + + public Project getProjectByName(String name, boolean noerror) { + Optional project = this.projectRepository.findByProjectName(name); + if (project.isEmpty()) { + if (noerror) logger.error("No project found with name {}", name); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ce projet n'existe pas"); + } + return project.get(); + } + + public Project getProjectByName(String name) { + return getProjectByName(name, false); + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/ReportService.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/ReportService.java new file mode 100644 index 0000000..2a8d273 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/ReportService.java @@ -0,0 +1,60 @@ +package enseirb.myinpulse.service.database; + +import enseirb.myinpulse.model.Report; +import enseirb.myinpulse.repository.ReportRepository; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Optional; + +@Service +public class ReportService { + + protected static final Logger logger = LogManager.getLogger(); + + private final ReportRepository reportRepository; + + @Autowired + ReportService(ReportRepository reportRepository) { + this.reportRepository = reportRepository; + } + + public Iterable getAllReports() { + return this.reportRepository.findAll(); + } + + public Report getReportById(Long id) { + Optional report = this.reportRepository.findById(id); + if (report.isEmpty()) { + logger.error("getReportById : No report found with id {}", id); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ce compte rendu n'existe pas"); + } + return report.get(); + } + + // TODO: do some validation + public Report addNewReport(Report report) { + return this.reportRepository.save(report); + } + + public void deleteReportById(Long id) { + this.reportRepository.deleteById(id); + } + + public Report updateReport(Long id, String reportContent) { + Optional report = this.reportRepository.findById(id); + if (report.isEmpty()) { + logger.error("updateReport : No report found with id {}", id); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ce compte rendu n'existe pas"); + } + if (reportContent != null) { + report.get().setReportContent(reportContent); + } + return this.reportRepository.save(report.get()); + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/SectionCellService.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/SectionCellService.java new file mode 100644 index 0000000..59c7397 --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/SectionCellService.java @@ -0,0 +1,93 @@ +package enseirb.myinpulse.service.database; + +import enseirb.myinpulse.model.Appointment; +import enseirb.myinpulse.model.Project; +import enseirb.myinpulse.model.SectionCell; +import enseirb.myinpulse.repository.SectionCellRepository; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Service +public class SectionCellService { + + protected static final Logger logger = LogManager.getLogger(); + + private final SectionCellRepository sectionCellRepository; + + @Autowired + SectionCellService(SectionCellRepository sectionCellRepository) { + this.sectionCellRepository = sectionCellRepository; + } + + public Iterable getAllSectionCells() { + return this.sectionCellRepository.findAll(); + } + + public SectionCell getSectionCellById(Long id) { + Optional sectionCell = this.sectionCellRepository.findById(id); + if (sectionCell.isEmpty()) { + logger.error("getSectionCellById : No sectionCell found with id {}", id); + throw new ResponseStatusException( + HttpStatus.NOT_FOUND, "Cette cellule de section n'existe pas"); + } + return sectionCell.get(); + } + + public SectionCell addNewSectionCell(SectionCell sectionCell) { + return this.sectionCellRepository.save(sectionCell); + } + + public void removeSectionCellById(Long id) { + this.sectionCellRepository.deleteById(id); + } + + public SectionCell updateSectionCell( + Long id, Long sectionId, String contentSectionCell, LocalDateTime modificationDate) { + Optional sectionCell = this.sectionCellRepository.findById(id); + if (sectionCell.isEmpty()) { + logger.error("updateSectionCell : No sectionCell found with id {}", id); + throw new ResponseStatusException( + HttpStatus.NOT_FOUND, "Cette cellule de section n'existe pas"); + } + if (sectionId != null) { + sectionCell.get().setSectionId(sectionId); + } + if (contentSectionCell != null) { + sectionCell.get().setContentSectionCell(contentSectionCell); + } + if (modificationDate != null) { + sectionCell.get().setModificationDate(modificationDate); + } + return this.sectionCellRepository.save(sectionCell.get()); + } + + public Iterable getSectionCellsByProject(Project project, Long sectionId) { + return this.sectionCellRepository.findByProjectSectionCellAndSectionId(project, sectionId); + } + + public Long getProjectId(Long sectionCellId) { + SectionCell sectionCell = getSectionCellById(sectionCellId); + Project sectionProject = sectionCell.getProjectSectionCell(); + return sectionProject.getIdProject(); + } + + public List getAppointmentsBySectionCellId(Long sectionCellId) { + SectionCell sectionCell = getSectionCellById(sectionCellId); + return sectionCell.getAppointmentSectionCell(); + } + + public Iterable getSectionCellsByProjectAndSectionIdBeforeDate( + Project project, long sectionId, LocalDateTime date) { + return sectionCellRepository.findByProjectSectionCellAndSectionIdAndModificationDateBefore( + project, sectionId, date); + } +} diff --git a/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/UserService.java b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/UserService.java new file mode 100644 index 0000000..45a4eac --- /dev/null +++ b/MyINPulse-back/src/main/java/enseirb/myinpulse/service/database/UserService.java @@ -0,0 +1,81 @@ +package enseirb.myinpulse.service.database; + +import enseirb.myinpulse.model.User; +import enseirb.myinpulse.repository.UserRepository; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Optional; + +@Service +public class UserService { + + protected static final Logger logger = LogManager.getLogger(); + + private final UserRepository userRepository; + + @Autowired + UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public Iterable getAllUsers() { + return this.userRepository.findAll(); + } + + // TODO + public User getUserByEmail(String email) { + Optional opt_user = this.userRepository.findByPrimaryMail(email); + + if (opt_user.isEmpty()) { + logger.error("getUserByEmail : No user found with email {}", email); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Cet utilisateur n'existe pas"); + } + return opt_user.get(); + } + + public Iterable allUsers() { + return this.userRepository.findAll(); + } + + public User addUser(@RequestBody User user) { + return this.userRepository.save(user); + } + + public User updateUser( + @PathVariable Long id, + String userSurname, + String userName, + String primaryMail, + String secondaryMail, + String phoneNumber) { + Optional user = userRepository.findById(id); + if (user.isEmpty()) { + logger.error("updateUser : No user found with id {}", id); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Cet utilisateur n'existe pas"); + } + if (userName != null) { + user.get().setUserName(userName); + } + if (userSurname != null) { + user.get().setUserSurname(userSurname); + } + if (primaryMail != null) { + user.get().setPrimaryMail(primaryMail); + } + if (secondaryMail != null) { + user.get().setSecondaryMail(secondaryMail); + } + if (phoneNumber != null) { + user.get().setPhoneNumber(phoneNumber); + } + return this.userRepository.save(user.get()); + } +} diff --git a/MyINPulse-back/src/main/resources/application.properties b/MyINPulse-back/src/main/resources/application.properties index 6d6825f..043c22b 100644 --- a/MyINPulse-back/src/main/resources/application.properties +++ b/MyINPulse-back/src/main/resources/application.properties @@ -1,4 +1,8 @@ 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 -logging.level.org.springframework.security=DEBUG +spring.datasource.url=jdbc:postgresql://${DATABASE_URL}/${BACKEND_DB} +spring.datasource.username=${BACKEND_USER} +spring.datasource.password=${BACKEND_PASSWORD} +spring.jpa.hibernate.ddl-auto=update +logging.pattern.console=%d{yyyy-MMM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n \ No newline at end of file diff --git a/MyINPulse-back/src/main/resources/data.sql b/MyINPulse-back/src/main/resources/data.sql new file mode 100644 index 0000000..444f8da --- /dev/null +++ b/MyINPulse-back/src/main/resources/data.sql @@ -0,0 +1,99 @@ +TRUNCATE project, user_inpulse, entrepreneur, administrator, section_cell, appointment, report, annotation CASCADE; + +SELECT setval('annotation_id_annotation_seq', 1, false); +SELECT setval('appointment_id_appointment_seq', 1, false); +SELECT setval('make_appointment_id_make_appointment_seq', 1, false); +SELECT setval('project_id_project_seq', 1, false); +SELECT setval('report_id_report_seq', 1, false); +SELECT setval('section_cell_id_section_cell_seq', 1, false); +SELECT setval('user_inpulse_id_user_seq', 1, false); + +INSERT INTO user_inpulse (user_surname, user_name, primary_mail, secondary_mail, phone_number) +VALUES ('Dupont', 'Dupond', 'super@mail.fr', 'super2@mail.fr', '06 45 72 45 98'), + ('Martin', 'Matin', 'genial@mail.fr', 'genial2@mail.fr', '06 52 14 58 73'), + ('Charvet', 'Lautre', 'mieux@tmail.fr', 'mieux2@tmail.fr', '07 49 82 16 35'), + ('Leguez', 'Theo', 'bof@mesmails.fr', 'bof2@mesmails.fr', '+33 6 78 14 25 29'), + ('Kia', 'Bi', 'special@mail.fr', 'special2@mail.fr', '07 65 31 38 95'), + ('Ducaillou', 'Pierre', 'maildefou@xyz.fr', 'maildefou2@xyz.fr', '06 54 78 12 62'), + ('Janine', 'Dave', 'janine@labri.fr', 'janine2@labri.fr', '06 87 12 45 95'); + +INSERT INTO administrator (id_administrator) +VALUES (7); + +INSERT INTO project (project_name, logo, creation_date, project_status, id_administrator) +VALUES ('Eau du robinet', decode('013d7d16d7ad4fefb61bd95b765c8ceb', 'hex'), TO_DATE('01-OCT-2023', 'DD-MON-YYYY'), + 'En cours', 7), + ('Air oxygéné', decode('150647a0984e8f228cd14b54', 'hex'), TO_DATE('04-APR-2024', 'DD-MON-YYYY'), 'En cours', 7), + ('Débat concours', decode('022024abd5486e245c145dda65116f', 'hex'), TO_DATE('22-NOV-2023', 'DD-MON-YYYY'), + 'Suspendu', 7), + ('HDeirbMI', decode('ab548d6c1d595a2975e6476f544d14c55a', 'hex'), TO_DATE('07-DEC-2024', 'DD-MON-YYYY'), + 'Lancement', 7); + + +INSERT INTO entrepreneur (school, course, snee_status, id_entrepreneur, id_project_participation, id_project_proposed) +VALUES ('ENSEIRB-MATMECA', 'INFO', TRUE, 1, 4, 4), + ('ENSC', 'Cognitique', TRUE, 2, 2, null), + ('ENSEIRB-MATMECA', 'MATMECA', FALSE, 3, 3, 3), + ('SupOptique', 'Classique', TRUE, 4, 1, 1), + ('ENSEGID', 'Géoscience', FALSE, 5, 1, null), + ('ENSMAC', 'Matériaux composites - Mécanique', FALSE, 6, 2, 2); + + +INSERT INTO section_cell (title, content_section_cell, modification_date, id_project) +VALUES ('Problème', 'les problèmes...', TO_TIMESTAMP('15-JAN-2025 09:30:20', 'DD-MON-YYYY, HH24:MI:SS'), 2), + ('Segment de client', 'Le segment AB passant le client n°8 est de longueur 32mm. + Le segment BC a quant à lui un longueur de 28mm. Quelle la longueur du segment AC ?', + TO_TIMESTAMP('12-OCT-2022 17:47:38', 'DD-MON-YYYY, HH24:MI:SS'), 3), + ('Proposition de valeur unique', '''Son prix est de 2594€'' ''Ah oui c''est unique en effet', + TO_TIMESTAMP('25-MAY-2024 11:12:04', 'DD-MON-YYYY, HH24:MI:SS'), 2), + ('Solution', 'Un problème ? Une solution', TO_TIMESTAMP('08-FEB-2024 10:17:53', 'DD-MON-YYYY, HH24:MI:SS'), 1), + ('Canaux', 'Ici nous avons la Seine, là-bas le Rhin, oh et plus loin le canal de Suez', + TO_TIMESTAMP('19-JUL-2023 19:22:45', 'DD-MON-YYYY, HH24:MI:SS'), 4), + ('Sources de revenus', 'Y''en n''a pas on est pas payé. Enfin y''a du café quoi', + TO_TIMESTAMP('12-JAN-2025 11:40:26', 'DD-MON-YYYY, HH24:MI:SS'), 1), + ('Structure des coûts', '''Ah oui là ça va faire au moins 1000€ par mois'', Eirbware', + TO_TIMESTAMP('06-FEB-2025 13:04:06', 'DD-MON-YYYY, HH24:MI:SS'), 3), + ('Indicateurs clés', 'On apprend les clés comme des badges, ça se fait', + TO_TIMESTAMP('05-FEB-2025 12:42:38', 'DD-MON-YYYY, HH24:MI:SS'), 4), + ('Avantages concurrentiel', 'On est meilleur', TO_TIMESTAMP('23-APR-2024 16:24:02', 'DD-MON-YYYY, HH24:MI:SS'), + 2); + +INSERT INTO appointment (appointment_date, appointment_time, appointment_duration, appointment_place, + appointment_subject) +VALUES (TO_DATE('24-DEC-2023', 'DD-MON-YYYY'), '00:00:00', '00:37:53', 'À la maison', 'Ouvrir les cadeaux'), + (TO_DATE('15-AUG-2024', 'DD-MON-YYYY'), '22:35:00', '00:12:36', 'Sur les quais ou dans un champ probablement', + 'BOUM BOUM les feux d''artifices (on fête quoi déjà ?)'), + (TO_DATE('28-FEB-2023', 'DD-MON-YYYY'), '14:20:00', '00:20:00', 'Salle TD 15', + 'Ah mince c''est pas une année bissextile !'), + (TO_DATE('23-JAN-2024', 'DD-MON-YYYY'), '12:56:27', '11:03:33', 'Là où le vent nous porte', + 'Journée la plus importante de l''année'), + (TO_DATE('25-AUG-2025', 'DD-MON-YYYY'), '00:09:00', '01:00:00', 'Euh c''est par où l''amphi 56 ?', + 'Rentrée scolaire (il fait trop froid c''est quoi ça on est en août)'); + +INSERT INTO report (report_content, id_appointment) +VALUES ('Ah oui ça c''est super, ah ouais j''aime bien, bien vu de penser à ça', 1), + ('Bonne réunion', 3), + ('Ouais, j''ai rien compris mais niquel on fait comme vous avez dit', 3), + ('Non non ça va pas du tout ce que tu me proposes, faut tout refaire', 4), + ('Réponse de la DSI : non', 2), + ('Trop dommage qu''Apple ait sorti leur logiciel avant nous, on avait la même idée et tout on aurait tellement pu leur faire de la concurrence', + 5); + +INSERT INTO annotation (comment, id_administrator, id_section_cell) +VALUES ('faut changer ça hein', 7, 5), + ('??? sérieusement, vous pensez que c''est une bonne idée ?', 7, 7), + ('ok donc ça c''est votre business plan, bah glhf la team', 7, 2); + + + + + + + + + + + + + + diff --git a/MyINPulse-back/src/main/resources/delete.sql b/MyINPulse-back/src/main/resources/delete.sql new file mode 100644 index 0000000..701b1f3 --- /dev/null +++ b/MyINPulse-back/src/main/resources/delete.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS administrateurs, projets, utilisateurs, entrepreneurs, sections, rendez_vous, comptes_rendus, concerner CASCADE; +DROP TABLE IF EXISTS administrator, project, user_inpulse, entrepreneur, section_cell, appointment, make_appointment, report, annotation, concern CASCADE; \ No newline at end of file diff --git a/MyINPulse-back/src/test/java/enseirb/myinpulse/AdminApiServiceTest.java b/MyINPulse-back/src/test/java/enseirb/myinpulse/AdminApiServiceTest.java new file mode 100644 index 0000000..676dfc9 --- /dev/null +++ b/MyINPulse-back/src/test/java/enseirb/myinpulse/AdminApiServiceTest.java @@ -0,0 +1,224 @@ +package enseirb.myinpulse; + +import static enseirb.myinpulse.model.ProjectDecisionValue.*; + +import static org.junit.jupiter.api.Assertions.*; + +import enseirb.myinpulse.model.Administrator; +import enseirb.myinpulse.model.Entrepreneur; +import enseirb.myinpulse.model.Project; +import enseirb.myinpulse.model.ProjectDecision; +import enseirb.myinpulse.service.AdminApiService; +import enseirb.myinpulse.service.database.AdministratorService; +import enseirb.myinpulse.service.database.EntrepreneurService; +import enseirb.myinpulse.service.database.ProjectService; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +@SpringBootTest +@Transactional +public class AdminApiServiceTest { + private static long administratorid; + private static Administrator administrator; + private static Entrepreneur entrepreneur; + @Autowired private AdminApiService adminApiService; + @Autowired private ProjectService projectService; + + @BeforeAll + static void setup( + @Autowired AdministratorService administratorService, + @Autowired ProjectService projectService, + @Autowired EntrepreneurService entrepreneurService) { + administratorService.addAdministrator( + new Administrator( + "admin", + "admin", + "testAdminEmpty@example.com", + "testAdmin@example.com", + "")); + administrator = + administratorService.addAdministrator( + new Administrator( + "admin2", + "admin2", + "testAdminFull@example.com", + "testAdmin@example.com", + "")); + administratorid = administrator.getIdUser(); + entrepreneur = + new Entrepreneur( + "JeSuisUnEntrepreneurDeCompet", + "EtUé", + "Entrepreneur@inpulse.com", + "mail2", + "phone", + "Ensimag nan jdeconne ENSEIRB (-matmeca mais on s'en fout)", + "info ofc", + false); + entrepreneurService.addEntrepreneur(entrepreneur); + projectService.addNewProject( + new Project( + "sampleProjectAdminApiService", + null, + LocalDate.now(), + ACTIVE, + administratorService.getAdministratorByPrimaryMain( + "testAdminFull@example.com"))); + } + + private List IterableToList(Iterable iterable) { + List l = new ArrayList<>(); + iterable.forEach(l::add); + return l; + } + + @Test + void getProjectOfAdminIsEmpty() { + Iterable projects = + adminApiService.getProjectsOfAdmin("testAdminEmpty@example.com"); + assertEquals(0, IterableToList(projects).size()); + } + + @Test + void getProjectOfInexistantAdminFails() { + String nonExistentAdminEmail = "testInexistantAdmin@example.com"; + + assertThrows( + ResponseStatusException.class, + () -> { + adminApiService.getProjectsOfAdmin(nonExistentAdminEmail); + }); + } + + @Test + void getProjectOfAdminNotEmpty() { + Iterable projects = + adminApiService.getProjectsOfAdmin("testAdminFull@example.com"); + List l = IterableToList(projects); + assertEquals(1, l.size()); + Project p = l.getFirst(); + assertEquals(p.getProjectName(), "sampleProjectAdminApiService"); + } + + @Test + void getPendingProjectsEmpty() { + assertEquals(0, IterableToList(this.adminApiService.getPendingProjects()).size()); + } + + @Test + void getPendingProjectsNotEmpty() { + this.projectService.addNewProject( + new Project( + "PendingProjectAdminApiService1", null, LocalDate.now(), PENDING, null)); + this.projectService.addNewProject( + new Project( + "PendingProjectAdminApiService2", null, LocalDate.now(), PENDING, null)); + Iterable pendingProjects = this.adminApiService.getPendingProjects(); + List pendingProjectsList = IterableToList(pendingProjects); + assertEquals(2, pendingProjectsList.size()); + assertTrue( + List.of("PendingProjectAdminApiService1", "PendingProjectAdminApiService2") + .contains(pendingProjectsList.getFirst().getProjectName())); + assertTrue( + List.of("PendingProjectAdminApiService1", "PendingProjectAdminApiService2") + .contains(pendingProjectsList.getLast().getProjectName())); + } + + @Test + void validateInexistantProject() { + ProjectDecision d = new ProjectDecision(-1, 0, 1); + assertThrows(ResponseStatusException.class, () -> this.adminApiService.validateProject(d)); + } + + @Test + void validateExistantProject() { + Project p = + new Project("PendingProjectAdminApiService2", null, LocalDate.now(), PENDING, null); + this.projectService.addNewProject(p); + assertEquals(PENDING, p.getProjectStatus()); + ProjectDecision d = new ProjectDecision(p.getIdProject(), administratorid, 1); + this.adminApiService.validateProject(d); + assertEquals(ACTIVE, p.getProjectStatus()); + + // Check if the project was really updated in the database + assertEquals(0, IterableToList(this.adminApiService.getPendingProjects()).size()); + } + + @Test + void refuseExistantProject() { + Project p = + new Project("PendingProjectAdminApiService2", null, LocalDate.now(), PENDING, null); + this.projectService.addNewProject(p); + assertEquals(PENDING, p.getProjectStatus()); + ProjectDecision d = new ProjectDecision(p.getIdProject(), administratorid, 0); + this.adminApiService.validateProject(d); + assertEquals(REJECTED, p.getProjectStatus()); + + // Check if the project was really updated in the database + assertEquals(0, IterableToList(this.adminApiService.getPendingProjects()).size()); + } + + @Test + void addProject() { + assertEquals(0, IterableToList(this.adminApiService.getPendingProjects()).size()); + Project p1 = + new Project("PendingProjectAdminApiService2", null, LocalDate.now(), PENDING, null); + this.adminApiService.addNewProject(p1); + + assertEquals(1, IterableToList(this.adminApiService.getPendingProjects()).size()); + } + + @Test + void addProjectToAdmin() { + assertEquals(0, administrator.getListProject().size()); + Project p1 = new Project("assProjectToAdmin", null, LocalDate.now(), ACTIVE, administrator); + this.adminApiService.addNewProject(p1); + assertEquals(1, administrator.getListProject().size()); + } + + @Test + void addProjectToUser() { + assertNull(entrepreneur.getProjectParticipation()); + Project p1 = + new Project("assProjectToAdmin", null, LocalDate.now(), ACTIVE, null, entrepreneur); + this.adminApiService.addNewProject(p1); + assertEquals(p1, entrepreneur.getProjectParticipation()); + } + + @Test + void addProjectWithManyUsers() { + Entrepreneur e1 = new Entrepreneur(); + Entrepreneur e2 = new Entrepreneur(); + Entrepreneur e3 = new Entrepreneur(); + assertNull(e1.getProjectParticipation()); + assertNull(e2.getProjectParticipation()); + assertNull(e3.getProjectParticipation()); + Project p1 = new Project("assProjectToAdmin", null, LocalDate.now(), ACTIVE, null, null); + p1.updateListEntrepreneurParticipation(e1); + p1.updateListEntrepreneurParticipation(e2); + p1.updateListEntrepreneurParticipation(e3); + this.adminApiService.addNewProject(p1); + assertEquals(p1, e1.getProjectParticipation()); + assertEquals(p1, e2.getProjectParticipation()); + assertEquals(p1, e3.getProjectParticipation()); + } + + @Test + void addDuplicateProject() { + Project p1 = + new Project("PendingProjectAdminApiService2", null, LocalDate.now(), PENDING, null); + Project p2 = + new Project("PendingProjectAdminApiService2", null, LocalDate.now(), PENDING, null); + this.adminApiService.addNewProject(p1); + assertThrows(ResponseStatusException.class, () -> this.adminApiService.addNewProject(p2)); + } +} diff --git a/MyINPulse-back/src/test/java/enseirb/myinpulse/MyinpulseApplicationTests.java b/MyINPulse-back/src/test/java/enseirb/myinpulse/MyinpulseApplicationTests.java index dce5ab2..c97449e 100644 --- a/MyINPulse-back/src/test/java/enseirb/myinpulse/MyinpulseApplicationTests.java +++ b/MyINPulse-back/src/test/java/enseirb/myinpulse/MyinpulseApplicationTests.java @@ -1,13 +1,13 @@ package enseirb.myinpulse; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class MyinpulseApplicationTests { - @Test - void contextLoads() { - } - + @Test + @DisplayName("contextLoad => Test if the context can load, i.e. the application can start") + void contextLoads() {} } diff --git a/MyINPulse-back/src/test/resources/application.properties b/MyINPulse-back/src/test/resources/application.properties new file mode 100644 index 0000000..a27c24b --- /dev/null +++ b/MyINPulse-back/src/test/resources/application.properties @@ -0,0 +1,10 @@ +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1 +spring.datasource.username=sa +spring.datasource.password=sa +spring.sql.init.mode=never +spring.application.name=myinpulse-test +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 +spring.jpa.hibernate.ddl-auto=update +logging.pattern.console=%d{yyyy-MMM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n \ No newline at end of file diff --git a/config/.env.dev b/config/.env.dev new file mode 100644 index 0000000..bcd45f3 --- /dev/null +++ b/config/.env.dev @@ -0,0 +1,22 @@ +POSTGRES_DB=postgres_db +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres_db_user_password + +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=admin +KEYCLOAK_HOSTNAME=localhost +KEYCLOAK_DB=keycloak_db +KEYCLOAK_USER=keycloak_db_user +KEYCLOAK_PASSWORD=keycloak_db_user_password + +BACKEND_DB=backend_db +BACKEND_USER=backend_db_user +BACKEND_PASSWORD=backend_db_user_password + +DATABASE_URL=localhost:5433 + +VITE_KEYCLOAK_URL=http://localhost:7080 +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/backdev.docker-compose.yaml b/config/backdev.docker-compose.yaml index a73a010..25151a0 100644 --- a/config/backdev.docker-compose.yaml +++ b/config/backdev.docker-compose.yaml @@ -1,15 +1,14 @@ services: postgres: - image: postgres:latest + env_file: .env + build: + context: postgres/ + dockerfile: Dockerfile container_name: MyINPulse-DB - #ports: - # - 5432:5432 + ports: + - 5433:5432 volumes: - - ./postgres:/var/lib/postgresql/data - environment: - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + - ./postgres/data:/var/lib/postgresql/data keycloak: container_name: MyINPulse-keycloak diff --git a/config/backdev.env b/config/backdev.env new file mode 100644 index 0000000..a7e5517 --- /dev/null +++ b/config/backdev.env @@ -0,0 +1,22 @@ +POSTGRES_DB=postgres_db +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres_db_user_password + +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=admin +KEYCLOAK_HOSTNAME=localhost +KEYCLOAK_DB=keycloak_db +KEYCLOAK_USER=keycloak_db_user +KEYCLOAK_PASSWORD=keycloak_db_user_password + +BACKEND_DB=backend_db +BACKEND_USER=backend_db_user +BACKEND_PASSWORD=backend_db_user_password + +DATABASE_URL=localhost:5433 + +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/backdev.front.env b/config/backdev.front.env deleted file mode 100644 index 27cf54e..0000000 --- a/config/backdev.front.env +++ /dev/null @@ -1,5 +0,0 @@ -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/backdev.main.env b/config/backdev.main.env deleted file mode 100644 index 6f32124..0000000 --- a/config/backdev.main.env +++ /dev/null @@ -1,6 +0,0 @@ -POSTGRES_DB=keycloak_db -POSTGRES_USER=keycloak_db_user -POSTGRES_PASSWORD=keycloak_db_user_password -KEYCLOAK_ADMIN=admin -KEYCLOAK_ADMIN_PASSWORD=admin -KEYCLOAK_HOSTNAME=localhost \ No newline at end of file diff --git a/config/dev.docker-compose.yaml b/config/dev.docker-compose.yaml new file mode 100644 index 0000000..d66d92a --- /dev/null +++ b/config/dev.docker-compose.yaml @@ -0,0 +1,52 @@ +services: + postgres: + env_file: .env + build: + context: postgres/ + dockerfile: Dockerfile + container_name: MyINPulse-DB + ports: + - 5433:5432 + volumes: + - ./postgres/data:/var/lib/postgresql/data + + + keycloak: + container_name: MyINPulse-keycloak + build: + context: ./keycloak + dockerfile: Dockerfile + args: + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://postgres/${POSTGRES_DB} + KC_DB_USERNAME: ${POSTGRES_USER} + KC_DB_PASSWORD: ${POSTGRES_PASSWORD} + environment: + KC_HOSTNAME_PORT: 7080 + KC_HOSTNAME_STRICT_BACKCHANNEL: "true" + KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN} + KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} + KC_LOG_LEVEL: info + command: ["start-dev", "--http-port", "7080", "--https-port", "7443", "--hostname", "${KEYCLOAK_HOSTNAME}"] + ports: + - "7080:7080" + - "7443:7443" + depends_on: + - postgres + + #front: + # build: + # context: ./front/ + # dockerfile: Dockerfile + # container_name: MyINPulse-front + # ports: + # - "8080:80" + + #back: + # build: + # context: ./MyINPulse-back/ + # dockerfile: Dockerfile + # container_name: MyINPulse-back + # ports: + # - "8081:8080" + \ No newline at end of file diff --git a/config/dev.env b/config/dev.env new file mode 100644 index 0000000..bcd45f3 --- /dev/null +++ b/config/dev.env @@ -0,0 +1,22 @@ +POSTGRES_DB=postgres_db +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres_db_user_password + +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=admin +KEYCLOAK_HOSTNAME=localhost +KEYCLOAK_DB=keycloak_db +KEYCLOAK_USER=keycloak_db_user +KEYCLOAK_PASSWORD=keycloak_db_user_password + +BACKEND_DB=backend_db +BACKEND_USER=backend_db_user +BACKEND_PASSWORD=backend_db_user_password + +DATABASE_URL=localhost:5433 + +VITE_KEYCLOAK_URL=http://localhost:7080 +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/frontdev.docker-compose.yaml b/config/frontdev.docker-compose.yaml index aa6d0d0..529ac2c 100644 --- a/config/frontdev.docker-compose.yaml +++ b/config/frontdev.docker-compose.yaml @@ -1,15 +1,15 @@ services: postgres: - image: postgres:latest + env_file: .env + build: + context: postgres/ + dockerfile: Dockerfile container_name: MyINPulse-DB #ports: # - 5432:5432 volumes: - - ./postgres:/var/lib/postgresql/data - environment: - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + - ./postgres/data:/var/lib/postgresql/data + keycloak: container_name: MyINPulse-keycloak diff --git a/config/frontdev.env b/config/frontdev.env new file mode 100644 index 0000000..ed4a23b --- /dev/null +++ b/config/frontdev.env @@ -0,0 +1,22 @@ +POSTGRES_DB=postgres_db +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres_db_user_password + +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=admin +KEYCLOAK_HOSTNAME=localhost +KEYCLOAK_DB=keycloak_db +KEYCLOAK_USER=keycloak_db_user +KEYCLOAK_PASSWORD=keycloak_db_user_password + +BACKEND_DB=backend_db +BACKEND_USER=backend_db_user +BACKEND_PASSWORD=backend_db_user_password + +DATABASE_URL=MyINPulse-DB + +VITE_KEYCLOAK_URL=http://localhost:7080 +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/frontdev.front.env b/config/frontdev.front.env deleted file mode 100644 index 5eba221..0000000 --- a/config/frontdev.front.env +++ /dev/null @@ -1,5 +0,0 @@ -VITE_KEYCLOAK_URL=http://localhost:7080 -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/frontdev.main.env b/config/frontdev.main.env deleted file mode 100644 index 6f32124..0000000 --- a/config/frontdev.main.env +++ /dev/null @@ -1,6 +0,0 @@ -POSTGRES_DB=keycloak_db -POSTGRES_USER=keycloak_db_user -POSTGRES_PASSWORD=keycloak_db_user_password -KEYCLOAK_ADMIN=admin -KEYCLOAK_ADMIN_PASSWORD=admin -KEYCLOAK_HOSTNAME=localhost \ No newline at end of file diff --git a/config/prod.docker-compose.yaml b/config/prod.docker-compose.yaml index fe2a3d7..496efb3 100644 --- a/config/prod.docker-compose.yaml +++ b/config/prod.docker-compose.yaml @@ -1,11 +1,14 @@ services: postgres: - image: postgres:latest + env_file: .env + build: + context: postgres/ + dockerfile: Dockerfile container_name: MyINPulse-DB #ports: # - 5432:5432 volumes: - - ./postgres:/var/lib/postgresql/data + - ./postgres/data:/var/lib/postgresql/data environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} @@ -27,10 +30,10 @@ services: KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN} KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} KC_LOG_LEVEL: info - command: ["start-dev", "--http-port", "7080", "--https-port", "7443", "--hostname", "${KEYCLOAK_HOSTNAME}"] - ports: - - "7080:7080" - - "7443:7443" + command: ["start-dev", "--http-port", "7080", "--https-port", "7443", "--hostname", "${KEYCLOAK_HOSTNAME}"] # TODO: remove start-dev + #ports: + # - "7080:7080" + # - "7443:7443" depends_on: - postgres @@ -47,6 +50,6 @@ services: context: ./MyINPulse-back/ dockerfile: Dockerfile container_name: MyINPulse-back - ports: - - "8081:8080" + #ports: + # - "8081:8080" \ No newline at end of file diff --git a/config/prod.env b/config/prod.env new file mode 100644 index 0000000..2fb8da9 --- /dev/null +++ b/config/prod.env @@ -0,0 +1,22 @@ +POSTGRES_DB=postgres_db +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres_db_user_password + +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=admin +KEYCLOAK_HOSTNAME=0549cd63f912d5dc9b31278d6f.eirb.fr +KEYCLOAK_DB=keycloak_db +KEYCLOAK_USER=keycloak_db_user +KEYCLOAK_PASSWORD=keycloak_db_user_password + +BACKEND_DB=backend_db +BACKEND_USER=backend_db_user +BACKEND_PASSWORD=backend_db_user_password + +DATABASE_URL=MyINPulse-DB + +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/config/prod.front.env b/config/prod.front.env deleted file mode 100644 index cb42a37..0000000 --- a/config/prod.front.env +++ /dev/null @@ -1,5 +0,0 @@ -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/config/prod.main.env b/config/prod.main.env deleted file mode 100644 index e8db94b..0000000 --- a/config/prod.main.env +++ /dev/null @@ -1,6 +0,0 @@ -POSTGRES_DB=keycloak_db -POSTGRES_USER=keycloak_db_user -POSTGRES_PASSWORD=keycloak_db_user_password -KEYCLOAK_ADMIN=admin -KEYCLOAK_ADMIN_PASSWORD=admin -KEYCLOAK_HOSTNAME=0549cd63f912d5dc9b31278d6f.eirb.fr \ No newline at end of file diff --git a/documentation/Doc.txt b/documentation/Doc.txt new file mode 100644 index 0000000..21b094a --- /dev/null +++ b/documentation/Doc.txt @@ -0,0 +1,12 @@ +Format des comptes rendus de réunion : +Texte organisé par bullet point, chaque bullet point est séparé par "//" pour pouvoir être correctement généré. + +Exemple : +Le texte "// blablabla // oui bonjour" +donne le résultat + +Point n°1 : + blablabla + +Point n°2 : + oui bonjour \ No newline at end of file diff --git a/front/MyINPulse-front/src/components/temp-modal.vue b/front/MyINPulse-front/src/components/temp-modal.vue new file mode 100644 index 0000000..a8dd50e --- /dev/null +++ b/front/MyINPulse-front/src/components/temp-modal.vue @@ -0,0 +1,18 @@ + + + + + diff --git a/front/MyINPulse-front/src/services/api.ts b/front/MyINPulse-front/src/services/api.ts index 091455c..5c4fc7b 100644 --- a/front/MyINPulse-front/src/services/api.ts +++ b/front/MyINPulse-front/src/services/api.ts @@ -14,7 +14,8 @@ axiosInstance.interceptors.response.use( async (error) => { const originalRequest = error.config; if ( - error.response.status === 401 && + ((error.response && error.response.status === 401) || + error.code == "ERR_NETWORK") && !originalRequest._retry && store.authenticated ) { diff --git a/front/MyINPulse-front/src/views/testComponent.vue b/front/MyINPulse-front/src/views/testComponent.vue index 9ba1d5a..1b1fd8a 100644 --- a/front/MyINPulse-front/src/views/testComponent.vue +++ b/front/MyINPulse-front/src/views/testComponent.vue @@ -1,6 +1,9 @@ diff --git a/hooks/README.md b/hooks/README.md new file mode 100644 index 0000000..c9eae1c --- /dev/null +++ b/hooks/README.md @@ -0,0 +1,2 @@ +# Useful hooks in this project +To use, just add the content of the wanted hook in `.git/hook/pre-commit`. You may need to use `chmod +x pre-commit` diff --git a/hooks/google-java-format b/hooks/google-java-format new file mode 100755 index 0000000..5155e6c --- /dev/null +++ b/hooks/google-java-format @@ -0,0 +1,24 @@ +#!/bin/bash + +# Path to the Google Java Formatter JAR +FORMATTER_JAR="$HOME/.local/share/java/google-java-format.jar" + +# Download the Google Java Formatter JAR if it doesn't exist +if [ ! -f "$FORMATTER_JAR" ]; then + echo "Downloading Google Java Formatter..." + mkdir -p "$(dirname "$FORMATTER_JAR")" + curl -L -o "$FORMATTER_JAR" https://github.com/google/google-java-format/releases/download/v1.20.0/google-java-format-1.20.0-all-deps.jar +fi + +# Format all staged Java files +STAGED_JAVA_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep "\.java$") + +if [ -n "$STAGED_JAVA_FILES" ]; then + echo "Formatting Java files..." + java -jar "$FORMATTER_JAR" --skip-sorting-imports --skip-reflowing-long-strings --aosp --replace $STAGED_JAVA_FILES + + # Re-stage the formatted files + git add $STAGED_JAVA_FILES +fi + +exit 0 diff --git a/postgres/Dockerfile b/postgres/Dockerfile new file mode 100644 index 0000000..da99686 --- /dev/null +++ b/postgres/Dockerfile @@ -0,0 +1,5 @@ +FROM postgres:latest + +# Custom initialization scripts +COPY ./create_user.sh /docker-entrypoint-initdb.d/10-create_user.sh +COPY ./create_db.sh /docker-entrypoint-initdb.d/20-create_db.sh diff --git a/postgres/create_db.sh b/postgres/create_db.sh new file mode 100644 index 0000000..c1728a0 --- /dev/null +++ b/postgres/create_db.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e + +POSTGRES="psql --username ${POSTGRES_USER}" + +echo "Creating database: ${DB_NAME}" + +$POSTGRES <