100 Commits

Author SHA1 Message Date
e3393c8834 test
All checks were successful
Format / formatting (push) Successful in 6s
Build / build (push) Successful in 28s
CI / build (push) Successful in 10s
2025-03-09 21:20:20 +01:00
5f8fe4a374 feat: precommit hook for google java format 2025-03-09 21:19:00 +01:00
c5e7736a16 fix: wtf does idea do ?? Why d methods move ? fixed linter again...
All checks were successful
Format / formatting (push) Successful in 5s
Build / build (push) Successful in 27s
CI / build (push) Successful in 10s
2025-03-09 21:10:25 +01:00
04589392cb fix: removed debug logging
Some checks failed
Format / formatting (push) Failing after 6s
Build / build (push) Successful in 29s
CI / build (push) Successful in 11s
2025-03-09 21:07:29 +01:00
1106cf8478 feat: added tests. 2025-03-09 21:06:31 +01:00
215d80ad70 fix: removed contradictive @NotNull preventing to add data to database. 2025-03-09 21:04:52 +01:00
dded62c25a fix: take latest implementation of logging module + imported inmemory database for testing 2025-03-09 20:22:20 +01:00
3de7ebe2b1 fix: remoed debug logging 2025-03-09 20:21:32 +01:00
8153496a0f feat: implemented database for testing purposes
All checks were successful
Format / formatting (push) Successful in 7s
Build / build (push) Successful in 28s
CI / build (push) Successful in 11s
2025-03-04 18:42:14 +01:00
861e7495a7 fix: re-enabled cache to drastically reduce action time. This should be fixed later
All checks were successful
Format / formatting (push) Successful in 6s
Build / build (push) Successful in 27s
CI / build (push) Successful in 10s
2025-03-01 01:00:52 +01:00
d78e43f7e0 fix: improved the workflow
Some checks failed
Format / formatting (push) Successful in 5s
Build / build (push) Has been cancelled
CI / build (push) Successful in 11s
2025-03-01 00:58:50 +01:00
3ca97cf378 fix: improved the workflow
All checks were successful
Format / formatting (push) Successful in 6s
Build / build (push) Successful in 28s
CI / build (push) Successful in 12s
2025-03-01 00:57:34 +01:00
e6a8d98d63 fix: improved the workflow
Some checks failed
Format / formatting (push) Successful in 6s
Build / build (push) Failing after 34s
CI / build (push) Successful in 11s
2025-02-28 12:20:48 +01:00
8894fea6d4 fix: improved the workflow
Some checks failed
Format / formatting (push) Successful in 6s
Build / build (push) Failing after 18s
CI / build (push) Successful in 10s
2025-02-28 12:20:05 +01:00
236bb0d167 fix: improved the workflow
Some checks failed
Format / formatting (push) Successful in 5s
Build / build (push) Failing after 6s
CI / build (push) Successful in 11s
2025-02-28 12:18:36 +01:00
d4dcc95d9b fix: removed cache to speed up things
Some checks failed
Format / formatting (push) Successful in 5s
Build / build (push) Failing after 14s
CI / build (push) Successful in 11s
2025-02-28 12:16:43 +01:00
dc843299eb fix: action create the gradlew wrapper
Some checks failed
Format / formatting (push) Successful in 5s
Build / build (push) Failing after 4m49s
CI / build (push) Successful in 11s
2025-02-28 12:10:13 +01:00
f3eaf8fe34 fix: action create the gradlew wrapper
Some checks failed
Format / formatting (push) Successful in 6s
Build / build (push) Failing after 4m46s
CI / build (push) Successful in 11s
2025-02-28 12:04:11 +01:00
628c61fb8b feat: pipeline should now test if the project builds
Some checks failed
Format / formatting (push) Successful in 5s
Build / build (push) Failing after 5m49s
CI / build (push) Successful in 10s
2025-02-28 11:55:43 +01:00
4880f3829c I don't get it, how does it keeps failing with the formatter installed... time to create pre-commit hook I guess
All checks were successful
Format / formatting (push) Successful in 6s
CI / build (push) Successful in 10s
2025-02-28 11:47:45 +01:00
80b2d087e4 feat: implemented date filtration and a utils service to prevent code ducplication
Some checks failed
Format / formatting (push) Failing after 7s
CI / build (push) Successful in 11s
2025-02-28 11:45:55 +01:00
b5c03798fc fix: formatter now follow the same logic as idea, see https://github.com/google/google-java-format/issues/566
All checks were successful
Format / formatting (push) Successful in 5s
CI / build (push) Successful in 10s
2025-02-26 18:55:45 +01:00
1d970ce5f5 feat: continued to implement SharedApiService (if linter fails i don't understand)
Some checks failed
Format / formatting (push) Failing after 6s
CI / build (push) Successful in 11s
2025-02-26 18:33:09 +01:00
f9de5ed6bf feat: finished creating services from controllers, continued implementing entrepreneurServiceApi with some validation
Some checks failed
Format / formatting (push) Failing after 6s
CI / build (push) Successful in 11s
2025-02-26 17:35:52 +01:00
e75a5c9d2c fix: linter ??? pls idea be better
All checks were successful
Format / formatting (push) Successful in 5s
CI / build (push) Successful in 12s
2025-02-26 15:57:03 +01:00
5c3b2b138d feat: renamex title to sectionId and added a new shared API call
Some checks failed
Format / formatting (push) Failing after 5s
CI / build (push) Successful in 11s
2025-02-26 15:55:33 +01:00
8d4dc7916d feat: added most of shared API calls
Some checks failed
Format / formatting (push) Failing after 5s
CI / build (push) Successful in 10s
2025-02-26 15:31:02 +01:00
1a6db7c953 feat: added color in logs
All checks were successful
Format / formatting (push) Successful in 6s
CI / build (push) Successful in 11s
2025-02-26 15:29:55 +01:00
3890aed158 Merge branch 'backend-api' of ssh://gitea.piair.dev:2222/piair/MyINPulse into backend-api
All checks were successful
Format / formatting (push) Successful in 5s
CI / build (push) Successful in 11s
2025-02-26 14:51:52 +01:00
dd5ca2cbd7 feat: entrepreneur api and api service 2025-02-26 14:51:02 +01:00
024deeba41 fix: linter - how did this append ?
All checks were successful
Format / formatting (push) Successful in 6s
CI / build (push) Successful in 10s
2025-02-26 14:30:58 +01:00
1cebebf1a5 feat: implemented most of the backend api for administrator
Some checks failed
Format / formatting (push) Failing after 6s
CI / build (push) Successful in 11s
2025-02-26 14:29:38 +01:00
d8bc7cc9b6 feat: added log4j. It's way better than System.stderr. 2025-02-26 14:28:31 +01:00
e66fa33577 feat: added a new database service
All checks were successful
Format / formatting (push) Successful in 7s
CI / build (push) Successful in 13s
2025-02-26 10:32:44 +01:00
27e70ee109 fix: naming issues with database and better data
All checks were successful
Format / formatting (push) Successful in 7s
CI / build (push) Successful in 11s
2025-02-24 17:19:18 +01:00
153501c8d4 Fix: merge
All checks were successful
Format / formatting (push) Successful in 7s
CI / build (push) Successful in 11s
2025-02-19 18:52:43 +01:00
40afde89b7 Fix: renamed all the tables, with repo and controller associated to them (might have missed some), and fix some key dependency issues 2025-02-19 18:41:37 +01:00
4698aa549f feat: frontend call now include the token and send the email from the token to backen servie
All checks were successful
Format / formatting (push) Successful in 6s
CI / build (push) Successful in 11s
2025-02-19 12:09:37 +01:00
04a73073c1 feat: switch to a service layer architecture
All checks were successful
Format / formatting (push) Successful in 6s
CI / build (push) Successful in 11s
2025-02-18 22:15:14 +01:00
11ab5e1dc4 feat: merged Keycloak / postgres
All checks were successful
Format / formatting (push) Successful in 7s
CI / build (push) Successful in 11s
2025-02-18 17:20:08 +01:00
820757c836 fix: corrected formatter error
All checks were successful
Format / formatting (push) Successful in 7s
CI / build (push) Successful in 11s
2025-02-18 16:59:19 +01:00
730aa5f450 Merge remote-tracking branch 'refs/remotes/origin/back-postgres' into back-postgres
Some checks failed
Format / formatting (push) Failing after 7s
CI / build (push) Successful in 11s
2025-02-18 16:56:26 +01:00
dda3e5fcfd fix: most likely fixed merge conflict 2025-02-18 16:55:08 +01:00
5e8e875a37 feat: added user deletion and custom api call in the frontend
All checks were successful
CI / build (push) Successful in 11s
2025-02-18 16:45:41 +01:00
86e7dc7c75 fix: removed the test file that was causing the linter to fail
All checks were successful
CI / build (push) Successful in 12s
2025-02-18 16:37:55 +01:00
6235fe7e68 feat: separated class definition
Some checks failed
CI / build (push) Failing after 8s
2025-02-18 12:07:07 +01:00
3cb12dab4f feat: created signature of all api functions
All checks were successful
Format / formatting (push) Successful in 8s
CI / build (push) Successful in 11s
2025-02-18 10:50:21 +01:00
0a9d83655f feat: query to database from and unaut endpoint
All checks were successful
Format / formatting (push) Successful in 9s
CI / build (push) Successful in 12s
2025-02-13 21:57:41 +01:00
fc73293122 fix: merge
All checks were successful
Format / formatting (push) Successful in 8s
CI / build (push) Successful in 13s
2025-02-12 19:03:39 +01:00
e26f8da662 fix: inserting data in db 2025-02-12 18:51:27 +01:00
b4c05f8c59 fix: formatting again
All checks were successful
Format / formatting (push) Successful in 8s
CI / build (push) Successful in 13s
2025-02-12 15:38:55 +01:00
ca282378ec fix: changed the formatting ?
Some checks failed
Format / formatting (push) Failing after 7s
CI / build (push) Successful in 13s
2025-02-12 15:34:16 +01:00
746fa97cf3 fix: applied formatter (on all import as well :)
Some checks failed
Format / formatting (push) Failing after 8s
CI / build (push) Successful in 13s
2025-02-12 15:29:12 +01:00
8b24456b48 feat: back to google as I can't make the other one work
Some checks failed
Format / formatting (push) Failing after 9s
CI / build (push) Successful in 13s
2025-02-12 15:14:48 +01:00
6befd10735 test: syntax validation
Some checks failed
Format / formatting (push) Failing after 1m24s
CI / build (push) Successful in 16s
2025-02-12 15:10:59 +01:00
db094a8d86 test: syntax validataion
Some checks failed
Format / formatting (push) Failing after 0s
CI / build (push) Successful in 12s
2025-02-12 15:09:42 +01:00
c739b4d26d test: syntax validataion
Some checks failed
Format / formatting (push) Failing after 3s
CI / build (push) Successful in 13s
2025-02-12 15:07:36 +01:00
dacb0dd179 fix: now uses another library
Some checks failed
CI / build (push) Successful in 52s
Format / formatting (push) Has been cancelled
2025-02-12 14:56:09 +01:00
b00c28a02a fix: now uses the correct version
Some checks failed
Format / formatting (push) Failing after 4s
CI / build (push) Successful in 12s
2025-02-12 14:54:27 +01:00
208cbbfa1d feat: now uses intellij
Some checks failed
Format / formatting (push) Failing after 3s
CI / build (push) Successful in 11s
2025-02-12 14:51:37 +01:00
d77f38b405 fix: removed exposed ports on the frontend
Some checks failed
Format / formatting (push) Failing after 7s
CI / build (push) Successful in 12s
2025-02-12 12:24:15 +01:00
525f98a054 feat: new makefile option 2025-02-12 12:23:53 +01:00
43aadac503 feat: reflected changes of path change 2025-02-12 12:23:04 +01:00
07f66f65ed feat: comments and security comfiguration improved. 2025-02-12 12:04:59 +01:00
6e5651c527 fix: remove dialect to supress a warning 2025-02-12 12:04:19 +01:00
1ed976b039 fix: now only show the incorrect files
Some checks failed
Format / formatting (push) Failing after 7s
CI / build (push) Successful in 12s
2025-02-12 11:42:16 +01:00
013b97cec0 test: check if the action is now red
Some checks failed
Format / formatting (push) Failing after 8s
CI / build (push) Successful in 12s
2025-02-12 11:34:57 +01:00
e7cb8cf469 feat: single .env file 2025-02-12 11:34:11 +01:00
184642a750 fix: coherent syntax 2025-02-12 11:32:41 +01:00
a8ae5f14d4 fix: db connection
All checks were successful
Format / formatting (push) Successful in 7s
CI / build (push) Successful in 12s
2025-02-12 11:09:17 +01:00
e6565275c8 fix: removed git action push to not destroy our history
All checks were successful
Format / formatting (push) Successful in 8s
CI / build (push) Successful in 12s
2025-02-12 11:04:49 +01:00
f629fb4a4e Google Java Format 2025-02-12 09:20:00 +00:00
7a9955b781 fix: merged
All checks were successful
Format / formatting (push) Successful in 9s
CI / build (push) Successful in 15s
2025-02-12 10:19:31 +01:00
1e97177777 fix: issue with foreign keys 2025-02-11 21:15:13 +01:00
9e2ab9fa5a Google Java Format 2025-02-11 18:27:36 +00:00
70b00a1996 feat: test on github validation
All checks were successful
Format / formatting (push) Successful in 8s
CI / build (push) Successful in 14s
2025-02-11 19:27:22 +01:00
720b19df93 wqMerge remote-tracking branch 'refs/remotes/origin/back-postgres' into back-postgres 2025-02-11 19:25:59 +01:00
1498b5908b fix: should now catch errors ? 2025-02-11 19:24:04 +01:00
45bbe51897 Google Java Format 2025-02-11 18:21:00 +00:00
c715955758 feat: the backend validator is better
All checks were successful
Format / formatting (push) Successful in 8s
CI / build (push) Successful in 13s
2025-02-11 19:20:49 +01:00
0fc4be2008 test: changed the verification
Some checks failed
Format / formatting (push) Failing after 44s
CI / build (push) Successful in 14s
2025-02-11 19:18:08 +01:00
c7ddc37bf9 test: changed the verification
Some checks failed
Format / formatting (push) Failing after 5s
CI / build (push) Successful in 14s
2025-02-11 19:12:49 +01:00
93bb46b017 test: check backend formatting
Some checks failed
check backend / formatting (push) Failing after 10s
CI / build (push) Successful in 12s
2025-02-11 19:04:02 +01:00
eed4e6f855 feat: multiple database and user in postgres
All checks were successful
CI / build (push) Successful in 11s
2025-02-11 11:07:45 +01:00
d2cc3e00e1 fix: Makefile now build correctly production environment 2025-02-11 10:07:00 +01:00
249d00177c feat: interraction between the backend and keycloak
Some checks failed
CI / build (push) Failing after 9s
2025-02-11 10:00:11 +01:00
36e4967394 Feat: first implementation of postgres db for backend
All checks were successful
CI / build (push) Successful in 13s
2025-02-11 00:08:53 +01:00
c32eea8a40 Merge pull request 'Mise en place d'un linter, de formatteur et d'actions afin de vérifier que le code du frontend compile bien' (#3) from linter into main
All checks were successful
CI / build (push) Successful in 12s
Reviewed-on: #3
Reviewed-by: Theo <tlelez@enseirb-matmeca.fr>
Vu que vous ne voulez pas faire avancer le projet, je vais abuser de mes droits d'administrateur et le faire.
2025-02-10 22:44:34 +01:00
30344a60b7 fix: removed import
All checks were successful
CI / build (push) Successful in 13s
2025-02-09 16:12:43 +01:00
2465545b6b fix: removed temp modal
Some checks failed
CI / build (push) Failing after 10s
2025-02-09 16:11:54 +01:00
83cbeb7a2e feat: single workflow that check prettier, linter and build
Some checks failed
CI / build (push) Failing after 11s
2025-02-09 16:07:40 +01:00
645a10477d feat: now respect codestyle
All checks were successful
CI / build (push) Successful in 4s
2025-02-09 15:59:30 +01:00
a859871265 fix: prettier now fail instead of doing nothing
Some checks failed
CI / build (push) Failing after 5s
2025-02-09 15:57:40 +01:00
0eab9a8063 feat: implemented prettier
All checks were successful
CI / build (push) Successful in 4s
2025-02-09 15:54:19 +01:00
1dff7573ff feat: now compliant with eslint
All checks were successful
CI / build (push) Successful in 10s
2025-02-09 15:28:09 +01:00
8af40bfe50 fix: linter action: installed required modules
Some checks failed
CI / build (push) Failing after 11s
2025-02-09 14:04:12 +01:00
afa4d34ec8 fix: linter action
Some checks failed
CI / build (push) Failing after 7s
2025-02-09 14:02:34 +01:00
c5fc5b600e fix: linter action
Some checks failed
CI / build (push) Failing after 12s
2025-02-09 14:00:49 +01:00
a4939737fe feat: trying to setup linter
Some checks failed
CI / build (push) Failing after 35s
2025-02-09 13:57:17 +01:00
e7ebcc0d3a feat: created eslint config 2025-02-09 12:36:43 +01:00
96 changed files with 4718 additions and 616 deletions

View File

@ -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"

View File

@ -0,0 +1,25 @@
name: Build
on:
push:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
- 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
- name: init gradle
working-directory: ./MyINPulse-back/
run: ./gradlew build -x test # todo: run test, currently fail because no database is present

View File

@ -0,0 +1,24 @@
name: CI
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install everything
working-directory: ./front/MyINPulse-front
run: npm i
- name: Run ESLint
working-directory: ./front/MyINPulse-front
run: npx eslint
- name: Run prettier
working-directory: ./front/MyINPulse-front
run: npx prettier src --check
- name: Build frontend
working-directory: ./front/MyINPulse-front
run: npm run build

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
.idea
keycloak/CAS/target
docker-compose.yaml
postgres/data

View File

@ -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:
@ -18,24 +21,37 @@ vite: ./front/MyINPulse-front/.installed
dev-front: clean vite
@cp config/frontdev.front.env front/MyINPulse-front/.env
@cp config/frontdev.main.env .env
@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
@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
@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 "cd MyINPulse-back" && echo 'export $$(cat .env | xargs)'
@echo "./gradlew bootRun --args='--server.port=8081'"
dev: clean vite
@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 &

View File

@ -20,7 +20,16 @@ repositories {
dependencies {
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'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'com.h2database:h2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

View File

@ -1,22 +1,10 @@
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 {
@ -24,6 +12,4 @@ public class MyinpulseApplication {
public static void main(String[] args) {
SpringApplication.run(MyinpulseApplication.class, args);
}
}

View File

@ -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;
}
}

View File

@ -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<Jwt, AbstractAuthenticationToken> {
/** 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<GrantedAuthority> tokenRolesExtractor(Jwt jwt) {
// Collection that will hold the extracted roles
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
// Realm roles
// Get the part of the access token that holds the roles assigned on realm level
Map<String, Collection<String>> 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<String> 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<GrantedAuthority> 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<String, Map<String, Collection<String>>> 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;
}
}

View File

@ -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.
*
* <p>admin endpoints are under /admin/* and entrepreneur are under /entrepreneur/*
*
* <p>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();
}
}

View File

@ -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<Project> 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<Appointment> 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<Project> getPendingProjects() {
return adminApiService.getPendingProjects();
}
/**
* Endpoint used to make a decision about a project.
*
* <p>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
*
* <p>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 String 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 ?
*
* <p>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);
}
}

View File

@ -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
*
* <p>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
*
* <p>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
*
* <p>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
*
* <p>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"));
}
}

View File

@ -0,0 +1,92 @@
package enseirb.myinpulse.controller;
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.*;
@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<SectionCell> 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<Entrepreneur> 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<Appointment> 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) {
sharedApiService.getPDFReport(appointmentId, principal.getClaimAsString("email"));
}
/**
* @return TODO
*/
@PostMapping("/shared/appointment/request")
public void createAppointmentRequest(
@RequestBody Appointment appointment, @AuthenticationPrincipal Jwt principal) {
sharedApiService.createAppointmentRequest(appointment, principal.getClaimAsString("email"));
}
}

View File

@ -0,0 +1,7 @@
package enseirb.myinpulse.exception;
public class RoleNotFoudException extends RuntimeException {
public RoleNotFoudException(String message) {
super(message);
}
}

View File

@ -0,0 +1,7 @@
package enseirb.myinpulse.exception;
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,42 @@
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<Project> listProject = new ArrayList<>();
/*@OneToMany(mappedBy = "administratorSectionCell", fetch = FetchType.LAZY, orphanRemoval = true)
private List<SectionCell> listSectionCell = new ArrayList<>();*/
// should now be useless
@OneToMany(mappedBy = "administratorAnnotation", fetch = FetchType.LAZY, orphanRemoval = true)
private final List<Annotation> listAnnotation = new ArrayList<>();
/*@OneToMany(mappedBy = "administratorAppointment", fetch = FetchType.LAZY, orphanRemoval = true)
private final List<Appointment> 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 mainMail,
String secondaryMail,
String phoneNumber) {
super(null, userSurname, username, mainMail, secondaryMail, phoneNumber);
}
}

View File

@ -0,0 +1,37 @@
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;
}
}

View File

@ -0,0 +1,118 @@
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<Entrepreneur> 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<SectionCell> 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<SectionCell> getAppointmentListSectionCell() {
return listSectionCell;
}
public Report getAppointmentReport() {
return report;
}
}

View File

@ -0,0 +1,82 @@
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(
Long idUser,
String userSurname,
String username,
String mainMail,
String secondaryMail,
String phoneNumber,
String school,
String course,
boolean sneeStatus) {
super(idUser, userSurname, username, mainMail, secondaryMail, phoneNumber);
this.school = school;
this.course = course;
this.sneeStatus = sneeStatus;
}
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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,101 @@
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<Entrepreneur> listEntrepreneurParticipation = new ArrayList<>();
@OneToMany(mappedBy = "projectSectionCell", fetch = FetchType.LAZY, orphanRemoval = true)
private final List<SectionCell> listSectionCell = new ArrayList<>();
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idProject;
@Column(length = 255)
private String projectName;
private byte[] logo;
private LocalDate creationDate;
@Column(length = 255)
private String 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(
Long idProject,
String projectName,
byte[] logo,
LocalDate creationDate,
String projectStatus) {
this.idProject = idProject;
this.projectName = projectName;
this.logo = logo;
this.creationDate = creationDate;
this.projectStatus = projectStatus;
}
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 String getProjectStatus() {
return projectStatus;
}
public void setProjectStatus(String projectStatus) {
this.projectStatus = projectStatus;
}
public Administrator getAdministrator() {
return this.projectAdministrator;
}
public void setAdministrator(Administrator administrator) {
this.projectAdministrator = administrator;
}
}

View File

@ -0,0 +1,7 @@
package enseirb.myinpulse.model;
public class ProjectDecision {
public long projectId;
public long adminId;
public long isAccepted;
}

View File

@ -0,0 +1,40 @@
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;
}
}

View File

@ -0,0 +1,7 @@
package enseirb.myinpulse.model;
public class RoleRepresentation {
public String id;
public String name;
public String description;
}

View File

@ -0,0 +1,88 @@
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<Appointment> appointment = new ArrayList<>();
@OneToMany(mappedBy = "sectionCellAnnotation", fetch = FetchType.LAZY, orphanRemoval = true)
private final List<Annotation> 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) {
this.idSectionCell = idSectionCell;
this.sectionId = sectionId;
this.contentSectionCell = contentSectionCell;
this.modificationDate = modificationDate;
}
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<Appointment> getAppointmentSectionCell() {
return appointment;
}
}

View File

@ -0,0 +1,93 @@
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() {}
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 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 mainMail) {
this.primaryMail = mainMail;
}
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;
}
}

View File

@ -0,0 +1,6 @@
package enseirb.myinpulse.model;
public class UserRepresentation {
public String id;
public String name;
}

View File

@ -0,0 +1,14 @@
package enseirb.myinpulse.repository;
import enseirb.myinpulse.model.Administrator;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource
public interface AdministratorRepository extends JpaRepository<Administrator, Long> {
/* @Query("SELECT a from Administrators a")
Administrator findAllAdministrator(); */
}

View File

@ -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<Annotation, Long> {}

View File

@ -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<Appointment, Long> {}

View File

@ -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<Entrepreneur, Long> {
Iterable<Entrepreneur> getEntrepreneurByProjectParticipation(Project project);
/* @Query("SELECT e from Entrepreneur e")
Entrepreneur findAllEntrepreneurl(); */
}

View File

@ -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<MakeAppointment, Long> {}

View File

@ -0,0 +1,14 @@
package enseirb.myinpulse.repository;
import enseirb.myinpulse.model.Administrator;
import enseirb.myinpulse.model.Project;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource
public interface ProjectRepository extends JpaRepository<Project, Long> {
Iterable<Project> findByProjectAdministrator(Administrator administrator);
Iterable<Project> findByProjectStatus(String status);
}

View File

@ -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<Report, Long> {}

View File

@ -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<SectionCell, Long> {
Iterable<SectionCell> findByProjectSectionCellAndSectionId(Project project, long sectionId);
Iterable<SectionCell> findByProjectSectionCellAndSectionIdAndModificationDateBefore(
Project project, long sectionId, LocalDateTime date);
}

View File

@ -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<User, Long> {
Optional<User> findByPrimaryMail(String email);
/* @Query("SELECT u from User u")
User findAllUser(); */
}

View File

@ -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<Jwt, AbstractAuthenticationToken> {
/**
* 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<GrantedAuthority> TEMPORARNAME(Jwt jwt) {
// Collection that will hold the extracted roles
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
// Realm roles
// Get the part of the access token that holds the roles assigned on realm level
Map<String, Collection<String>> 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<String> 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<GrantedAuthority> 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<String, Map<String, Collection<String>>> 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;
}
}

View File

@ -0,0 +1,72 @@
package enseirb.myinpulse.service;
import enseirb.myinpulse.model.*;
import enseirb.myinpulse.service.database.AdministratorService;
import enseirb.myinpulse.service.database.ProjectService;
import enseirb.myinpulse.service.database.UserService;
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 AdminApiService {
private final ProjectService projectService;
private final UserService userService;
private final AdministratorService administratorService;
@Autowired
AdminApiService(
ProjectService projectService,
UserService userService,
AdministratorService administratorService) {
this.projectService = projectService;
this.userService = userService;
this.administratorService = administratorService;
}
// TODO: test
public Iterable<Project> getProjectsOfAdmin(String email) {
return projectService.getProjectsByAdminId(
administratorService.getAdministratorById(
this.userService.getUserByEmail(email).getIdUser()));
}
// TODO
public Iterable<Appointment> getUpcomingAppointments(String email) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
// TODO: test
public Iterable<Project> getPendingProjects() {
return this.projectService.getPendingProjects();
}
// TODO: test
public void validateProject(ProjectDecision decision) {
projectService.updateProject(
decision.projectId,
null,
null,
null,
"ACTIVE",
this.administratorService.getAdministratorById(decision.projectId));
}
// TODO: solve todo + test
public void addNewProject(Project project) {
projectService.addNewProject(project); // TODO: how can the front know the ID ?
}
// TODO
public void createAppointmentReport(String appointmentId, Report report, String email) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
// TODO: test
public void deleteProject(long projectId) {
this.projectService.deleteProjectById(projectId);
}
}

View File

@ -0,0 +1,120 @@
package enseirb.myinpulse.service;
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()));
sectionCellService.addNewSectionCell(sectionCell);
}
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);
}
}

View File

@ -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
*
* <p>Set a keycloak role to a keycloak user.
*
* <p>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();
}
}

View File

@ -0,0 +1,153 @@
package enseirb.myinpulse.service;
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.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<SectionCell> 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<Entrepreneur> 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.getAdministrator();
}
// TODO
public Iterable<Appointment> 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<SectionCell> sectionCells =
this.sectionCellService.getSectionCellsByProject(
projectService.getProjectById(projectId),
2L); // sectionId useless in this function ?
List<Appointment> appointments = new ArrayList<Appointment>();
sectionCells.forEach(
sectionCell -> {
appointments.addAll(
this.sectionCellService.getAppointmentsBySectionCellId(
sectionCell.getIdSectionCell()));
});
return appointments;
}
// TODO
public void getPDFReport(long appointmentId, String mail) {
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");
}
/* return this.appointmentService
.getAppointmentById(appointmentId)
.getAppointmentReport().getReportContent(); */
// generate pdf from this string, and format it to be decent looking
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
// TODO
public void createAppointmentRequest(Appointment appointment, String mail) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "Not implemented yet");
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,49 @@
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<Administrator> allAdministrators() {
return this.administratorRepository.findAll();
}
public Administrator getAdministratorById(long id) {
Optional<Administrator> 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 addAdministrator(Administrator administrator) {
return this.administratorRepository.save(administrator);
}
/*
public Administrator getAdministratorByProject(Project project) {
r
}
*/
}

View File

@ -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<Annotation> getAllAnnotations() {
return annotationRepository.findAll();
}
public Annotation getAnnotationById(Long id) {
Optional<Annotation> 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> 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());
}
}

View File

@ -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<Appointment> getallAppointments() {
return this.appointmentRepository.findAll();
}
public Appointment getAppointmentById(Long id) {
Optional<Appointment> 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> 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());
}
}

View File

@ -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<Entrepreneur> getAllEntrepreneurs() {
return this.entrepreneurRepository.findAll();
}
public Entrepreneur getEntrepreneurById(Long id) {
Optional<Entrepreneur> 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> 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<Entrepreneur> GetEntrepreneurByProject(Project project) {
return this.entrepreneurRepository.getEntrepreneurByProjectParticipation(project);
}
}

View File

@ -0,0 +1,105 @@
package enseirb.myinpulse.service.database;
import enseirb.myinpulse.model.Administrator;
import enseirb.myinpulse.model.Project;
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<Project> getAllProjects() {
return this.projectRepository.findAll();
}
public Project getProjectById(Long id) {
Optional<Project> 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<Project> 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,
String projectStatus,
Administrator administrator) {
Optional<Project> 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) {
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().setAdministrator(administrator);
}
return this.projectRepository.save(project.get());
}
public Boolean validateStatus(String status) {
return List.of("PENDING", "ACTIVE", "ENDED").contains(status);
}
public Iterable<Project> getPendingProjects() {
return this.projectRepository.findByProjectStatus("PENDING");
}
public void deleteProjectById(Long id) {
this.projectRepository.deleteById(id);
}
}

View File

@ -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<Report> getAllReports() {
return this.reportRepository.findAll();
}
public Report getReportById(Long id) {
Optional<Report> 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> 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());
}
}

View File

@ -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<SectionCell> getAllSectionCells() {
return this.sectionCellRepository.findAll();
}
public SectionCell getSectionCellById(Long id) {
Optional<SectionCell> 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> 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<SectionCell> 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<Appointment> getAppointmentsBySectionCellId(Long sectionCellId) {
SectionCell sectionCell = getSectionCellById(sectionCellId);
return sectionCell.getAppointmentSectionCell();
}
public Iterable<SectionCell> getSectionCellsByProjectAndSectionIdBeforeDate(
Project project, long sectionId, LocalDateTime date) {
return sectionCellRepository.findByProjectSectionCellAndSectionIdAndModificationDateBefore(
project, sectionId, date);
}
}

View File

@ -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<User> getAllUsers() {
return this.userRepository.findAll();
}
// TODO
public User getUserByEmail(String email) {
Optional<User> 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<User> 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 mainMail,
String secondaryMail,
String phoneNumber) {
Optional<User> 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 (mainMail != null) {
user.get().setPrimaryMail(mainMail);
}
if (secondaryMail != null) {
user.get().setSecondaryMail(secondaryMail);
}
if (phoneNumber != null) {
user.get().setPhoneNumber(phoneNumber);
}
return this.userRepository.save(user.get());
}
}

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -0,0 +1,51 @@
package enseirb.myinpulse;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import enseirb.myinpulse.model.Administrator;
import enseirb.myinpulse.model.Project;
import enseirb.myinpulse.service.AdminApiService;
import enseirb.myinpulse.service.database.AdministratorService;
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.util.ArrayList;
import java.util.List;
@SpringBootTest
@Transactional
public class AdminApiServiceTest {
@Autowired private AdminApiService adminApiService;
@BeforeAll
static void setup(@Autowired AdministratorService administratorService) {
administratorService.addAdministrator(
new Administrator(
"admin", "admin", "testAdmin@example.com", "testAdmin@example.com", ""));
}
@Test
void getProjectOfAdminIsEmpty() throws Exception {
Iterable<Project> projects = adminApiService.getProjectsOfAdmin("testAdmin@example.com");
List<Project> l = new ArrayList<>();
projects.forEach(l::add);
assertEquals(0, l.size());
}
@Test
void getProjectOfInexistantAdminFails() throws Exception {
String nonExistentAdminEmail = "testInexistantAdmin@example.com";
assertThrows(
ResponseStatusException.class,
() -> {
adminApiService.getProjectsOfAdmin(nonExistentAdminEmail);
});
}
}

View File

@ -1,5 +1,6 @@
package enseirb.myinpulse;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@ -7,7 +8,6 @@ import org.springframework.boot.test.context.SpringBootTest;
class MyinpulseApplicationTests {
@Test
void contextLoads() {
}
@DisplayName("contextLoad => Test if the context can load, i.e. the application can start")
void contextLoads() {}
}

View File

@ -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

View File

@ -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

22
config/backdev.env Normal file
View File

@ -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/

View File

@ -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/

View File

@ -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

View File

@ -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"

22
config/dev.env Normal file
View File

@ -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/

View File

@ -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

22
config/frontdev.env Normal file
View File

@ -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/

View File

@ -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/

View File

@ -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

View File

@ -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"

22
config/prod.env Normal file
View File

@ -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/

View File

@ -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/

View File

@ -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

View File

@ -36,4 +36,4 @@ playwright-report/
# Custom
.installed
package-lock.json
./package-lock.json

View File

@ -0,0 +1,7 @@
{
"useTabs":false,
"semi":true,
"trailingComma":"es5",
"arrowParens":"always",
"tabWidth":4
}

View File

@ -0,0 +1,29 @@
import eslint from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import eslintPluginVue from "eslint-plugin-vue";
import globals from "globals";
import typescriptEslint from "typescript-eslint";
export default typescriptEslint.config(
{ ignores: ["*.d.ts", "**/coverage", "**/dist"] },
{
extends: [
eslint.configs.recommended,
...typescriptEslint.configs.recommended,
...eslintPluginVue.configs["flat/recommended"],
],
files: ["**/*.{ts,vue}"],
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
globals: globals.browser,
parserOptions: {
parser: typescriptEslint.parser,
},
},
rules: {
// your rules
},
},
eslintConfigPrettier
);

File diff suppressed because it is too large Load Diff

View File

@ -26,8 +26,15 @@
"@types/node": "^22.10.7",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.20.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-vue": "^9.32.0",
"globals": "^15.14.0",
"jiti": "^2.4.2",
"npm-run-all2": "^7.0.2",
"prettier": "3.5.0",
"typescript": "~5.7.3",
"typescript-eslint": "^8.23.0",
"vite": "^6.0.11",
"vite-plugin-vue-devtools": "^7.7.0",
"vue-tsc": "^2.2.0"

View File

@ -1,44 +1,40 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import { RouterView } from "vue-router";
import ErrorWrapper from "@/views/errorWrapper.vue";
import ProjectComponent from "@/components/ProjectComponent.vue";
</script>
<template>
<Header />
<HeaderComponent />
<error-wrapper></error-wrapper>
<div id="main">
<ProjectComp
<ProjectComponent
v-for="(project, index) in projects"
:key="index"
:projectName="project.name"
:project-name="project.name"
/>
</div>
<RouterView />
</template>
<style scoped>
</style>
<script lang="ts">
import Header from "@/components/Header.vue";
import ProjectComp from "@/components/Project-comp.vue";
import HeaderComponent from "@/components/HeaderComponent.vue";
export default {
name: 'App',
name: "App",
components: {
Header,
ProjectComp,
HeaderComponent,
},
data() {
return {
projects: [
{
name: 'Projet Alpha',
name: "Projet Alpha",
//link: './project-alpha.html',
//members: ['Alice', 'Bob', 'Charlie'],
},
{
name: 'Projet Beta',
name: "Projet Beta",
//link: './project-beta.html',
//members: ['David', 'Eve', 'Frank'],
},
@ -46,5 +42,6 @@ export default {
};
},
};
</script>
<style scoped></style>

View File

@ -2,25 +2,25 @@
<header>
<img src="./icons/logo inpulse.png" alt="INPulse" />
</header>
</template>
</template>
<script lang="ts">
export default {
name: 'Header',
};
</script>
<script lang="ts">
export default {
name: "HeaderComponent",
};
</script>
<style scoped>
header img {
<style scoped>
header img {
width: 100px;
}
}
header{
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #fff;
border-bottom: 2px solid #ddd;
}
</style>
}
</style>

View File

@ -3,16 +3,19 @@
<div class="project-header">
<h2>{{ projectName }}</h2>
</div>
</div>
</template>
<script lang="ts">
import type { PropType } from "vue";
export default {
name: 'Project',
name: "ProjectComponent",
props: {
projectName: String,
}
}
projectName: {
type: Object as PropType<string>,
required: true,
},
},
};
</script>

View File

@ -1,17 +1,43 @@
<script setup lang="ts">
const props = defineProps(['data']);
import { color } from "@/services/popupDisplayer.ts";
import type { PropType } from "vue";
defineProps({
errorMessage: {
type: Object as PropType<string>,
required: true,
},
errorColor: {
type: Object as PropType<color>,
default: color.Red,
},
});
</script>
<template>
<div :class='["red", "yellow", "blue", "green"][data.type]' class="error-modal">
<p>{{["Erreur :(", "Warning :|", "Info :)", "Succes ;)"][data.type]}}</p>
<p>{{data.errorMessage}}</p>
<div class="loading" :class='["red-loader", "yellow-loader", "blue-loader", "green-loader"][data.type]'></div>
<div
:class="['red', 'yellow', 'blue', 'green'][errorColor]"
class="error-modal"
>
<p>
{{
["Erreur :(", "Warning :|", "Info :)", "Succes ;)"][errorColor]
}}
</p>
<p>{{ errorMessage }}</p>
<div
class="loading"
:class="
['red-loader', 'yellow-loader', 'blue-loader', 'green-loader'][
errorColor
]
"
></div>
</div>
</template>
<style scoped>
.error-modal{
.error-modal {
margin-bottom: 1em;
padding: 1em;
border-radius: 1em;
@ -19,41 +45,45 @@ const props = defineProps(['data']);
overflow: hidden;
position: relative;
animation: disappear 5s linear forwards;
}
}
.red{
.red {
background-color: #ee6055;
color: white;
}
.red-loader {
}
.red-loader {
background-color: #fa8383;
}
}
.yellow{
background-color: #FF9D23;
.yellow {
background-color: #ff9d23;
color: white;
}
.yellow-loader{
background-color: #ffbf81;
}
}
.blue {
.yellow-loader {
background-color: #ffbf81;
}
.blue {
background-color: #809bce;
color: white;
}
.blue-loader{
background-color: #95b8d1;
}
}
.green {
.blue-loader {
background-color: #95b8d1;
}
.green {
background-color: green;
color: white;
}
.green-loader {
background-color: darkgreen;
}
}
.loading {
.green-loader {
background-color: darkgreen;
}
.loading {
box-sizing: border-box;
position: absolute;
padding: 0;
@ -62,19 +92,19 @@ const props = defineProps(['data']);
height: 1em;
width: 0;
animation: loading 4s linear forwards;
}
}
/* Animation for the loading bar */
@keyframes loading {
/* Animation for the loading bar */
@keyframes loading {
0% {
width: 100%;
}
100% {
width: 0;
}
}
}
@keyframes disappear {
@keyframes disappear {
0% {
height: 100px;
padding: 1em;
@ -90,5 +120,5 @@ const props = defineProps(['data']);
margin: 0;
padding: 0;
}
}
}
</style>

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import { addNewMessage } from "@/services/popupDisplayer.ts";
</script>
<template>
<button
@click="
addNewMessage(
'new error from another view',
Math.floor(Math.random() * 4)
)
"
>
Add an error
</button>
</template>
<style scoped></style>

View File

@ -1,36 +1,31 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router/router.ts'
import {createPinia} from "pinia";
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import AuthStorePlugin from './plugins/authStore';
import keycloakService from './services/keycloak';
import {useAuthStore} from "@/stores/authStore.ts";
let store: any;
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router/router.ts";
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import keycloakService from "./services/keycloak";
import { type AuthStore, useAuthStore } from "@/stores/authStore.ts";
let store: AuthStore;
keycloakService.CallInit(() => {
try {
const app = createApp(App)
const app = createApp(App);
// Setup pinia store, allowing user to keep logged in status after refresh
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
app.use(pinia);
app.use(AuthStorePlugin, { pinia });
store = useAuthStore();
app.use(router)
keycloakService.CallInitStore(store);
app.use(router);
app.mount('#app');
app.mount("#app");
} catch (e) {
console.error("Error while initiating Keycloak.")
console.error(e)
createApp(App).mount('#app');
console.error("Error while initiating Keycloak.");
console.error(e);
createApp(App).mount("#app");
}
});
})
export {store};
export { store };

View File

@ -1,14 +0,0 @@
// file: src/plugins/authStore.js
import { useAuthStore } from "@/stores/authStore.ts";
import keycloakService from '@/services/keycloak';
// Setup auth store as a plugin so it can be accessed globally in our FE
const authStorePlugin = {
install(app: any, option: any) {
const store = useAuthStore(option.pinia);
app.config.globalProperties.$store = store;
keycloakService.CallInitStore(store);
}
}
export default authStorePlugin;

View File

@ -1,17 +1,17 @@
import { createRouter, createWebHistory } from 'vue-router'
import { createRouter, createWebHistory } from "vue-router";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/test',
name: 'test',
path: "/test",
name: "test",
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/test.vue'),
component: () => import("../views/testComponent.vue"),
},
],
})
});
export default router
export default router;

View File

@ -1,31 +1,37 @@
import axios from "axios";
import {store} from "@/main.ts";
import {addNewMessage, color} from "@/services/popupDisplayer.ts";
import axios, { type AxiosError, type AxiosResponse } from "axios";
import { store } from "@/main.ts";
import { addNewMessage, color } from "@/services/popupDisplayer.ts";
const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_BACKEND_URL,
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
});
axiosInstance.interceptors.response.use(
response => response, // Directly return successful responses.
async error => {
(response) => response, // Directly return successful responses.
async (error) => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry && store.authenticated) {
if (
((error.response && error.response.status === 401) ||
error.code == "ERR_NETWORK") &&
!originalRequest._retry &&
store.authenticated
) {
originalRequest._retry = true; // Mark the request as retried to avoid infinite loops.
try {
await store.refreshUserToken();
// Update the authorization header with the new access token.
axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${store.user.token}`;
axiosInstance.defaults.headers.common["Authorization"] =
`Bearer ${store.user.token}`;
return axiosInstance(originalRequest); // Retry the original request with the new access token.
} catch (refreshError) {
// Handle refresh token errors by clearing stored tokens and redirecting to the login page.
console.error('Token refresh failed:', refreshError);
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
console.error("Token refresh failed:", refreshError);
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
window.location.href = "/login";
return Promise.reject(refreshError);
}
}
@ -34,23 +40,29 @@ axiosInstance.interceptors.response.use(
);
// TODO: spawn a error modal
function defaultApiErrorHandler(err: string){
addNewMessage(err, color.Red);
function defaultApiErrorHandler(err: AxiosError) {
addNewMessage(err.message, color.Red);
}
function defaultApiSuccessHandler(response: any){
addNewMessage(response.data, color.green)
function defaultApiSuccessHandler(response: AxiosResponse) {
addNewMessage(response.data, color.Green);
}
function callApi(endpoint: string, onSuccessHandler?: any, onErrorHandler?: any): void {
axiosInstance.get(endpoint).then(
onSuccessHandler == null ? defaultApiSuccessHandler : onSuccessHandler
).catch(
(err) => {
onErrorHandler == null ? defaultApiErrorHandler(err): onErrorHandler(err);
throw err;
}
function callApi(
endpoint: string,
onSuccessHandler?: (response: AxiosResponse) => void,
onErrorHandler?: (error: AxiosError) => void
): void {
axiosInstance
.get(endpoint)
.then(
onSuccessHandler == null
? defaultApiSuccessHandler
: onSuccessHandler
)
.catch(
onErrorHandler == null ? defaultApiErrorHandler : onErrorHandler
);
}
export {callApi}
export { callApi };

View File

@ -1,33 +1,31 @@
import Keycloak from 'keycloak-js';
import Keycloak from "keycloak-js";
import type { AuthStore } from "@/stores/authStore.ts";
const options = {
url: import.meta.env.VITE_KEYCLOAK_URL,
clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID,
realm: import.meta.env.VITE_KEYCLOAK_REALM
}
realm: import.meta.env.VITE_KEYCLOAK_REALM,
};
const keycloak = new Keycloak(options);
let authenticated: boolean | undefined;
let store = null;
async function login(){
async function login() {
try {
await keycloak.login() // https://www.keycloak.org/securing-apps/javascript-adapter#:~:text=when%20initialization%20completes.-,login(options),-Redirects%20to%20login
await keycloak.login(); // https://www.keycloak.org/securing-apps/javascript-adapter#:~:text=when%20initialization%20completes.-,login(options),-Redirects%20to%20login
return keycloak;
} catch (error) {
console.log(error)
console.log(error);
}
}
async function signup(){
async function signup() {
try {
await keycloak.login(
{action: "register"}
) // https://www.keycloak.org/securing-apps/javascript-adapter#:~:text=when%20initialization%20completes.-,login(options),-Redirects%20to%20login
await keycloak.login({ action: "register" }); // https://www.keycloak.org/securing-apps/javascript-adapter#:~:text=when%20initialization%20completes.-,login(options),-Redirects%20to%20login
return keycloak;
} catch (error) {
console.log(error)
console.log(error);
}
}
@ -42,31 +40,33 @@ async function init(onInitCallback: () => void) {
onLoad: "check-sso",
silentCheckSsoRedirectUri: `${location.origin}/silent-check-sso.htm`,
responseMode: "query",
})
onInitCallback()
});
onInitCallback();
} catch (error) {
console.error("Keycloak init failed")
console.error(error)
console.error("Keycloak init failed");
console.error(error);
}
};
}
/**
* Initializes store with Keycloak user data
*
*/
async function initStore(storeInstance: any) {
async function initStore(storeInstance: AuthStore) {
try {
store = storeInstance
console.log(keycloak)
await store.initOauth(keycloak)
store = storeInstance;
console.log(keycloak);
await store.initOauth(keycloak);
// Show alert if user is not authenticated
if (!authenticated) { console.warn("not authenticated") }
} catch (error) {
console.error("Keycloak init failed")
console.error(error)
if (!authenticated) {
console.warn("not authenticated");
}
};
} catch (error) {
console.error("Keycloak init failed");
console.error(error);
}
}
/**
* Logout user
@ -83,7 +83,7 @@ async function refreshToken() {
await keycloak.updateToken(480);
return keycloak;
} catch (error) {
console.error('Failed to refresh token');
console.error("Failed to refresh token");
console.error(error);
}
}

View File

@ -1,19 +1,43 @@
import {ref} from "vue";
enum color {Red, Yellow, Blue, green}
import { ref, type Ref } from "vue";
function addNewMessage(errorMessage: string, type?: color, timeout?: number){
if (timeout == null){
enum color {
Red,
Yellow,
Blue,
Green,
}
type ErrorMessageContent = {
message: string;
color: color;
id: number;
timeout: number;
};
let id: number = 0;
const getId = () => {
id = id + 1;
return id;
};
function addNewMessage(errorMessage: string, type?: color, timeout?: number) {
if (timeout == null) {
timeout = 5000;
}
if (type == null){
if (type == null) {
type = color.Red;
}
const data = {errorMessage: errorMessage, timeout: timeout, type: type, uid: Math.random()*100000};
errorList.value.push(data)
setTimeout(() => errorList.value.slice(0, 1), timeout)
const data: ErrorMessageContent = {
message: errorMessage,
timeout: timeout,
color: type,
id: getId(),
};
errorList.value.push(data);
setTimeout(() => errorList.value.slice(0, 1), timeout);
}
const errorList: any= ref([])
const errorList: Ref<ErrorMessageContent[]> = ref([]);
export {addNewMessage, errorList, color}
export { addNewMessage, errorList, color, type ErrorMessageContent };

View File

@ -1,54 +1,61 @@
import { defineStore } from "pinia";
import keycloakService from '@/services/keycloak';
import keycloakService from "@/services/keycloak";
import type Keycloak from "keycloak-js";
export const useAuthStore = defineStore("storeAuth", {
const useAuthStore = defineStore("storeAuth", {
state: () => {
return {
testv: true,
authenticated: false,
user: {
token: "",
refreshToken: "",
username: "",
},
}
};
},
persist: true,
getters: {},
actions: {
// Initialize Keycloak OAuth
async initOauth(keycloak: Keycloak, clearData = true) {
if(clearData) { await this.clearUserData(); }
if (clearData) {
await this.clearUserData();
}
this.authenticated = !!keycloak.authenticated; // the !! removes undefined
if (this.authenticated && keycloak.token && keycloak.idTokenParsed && keycloak.refreshToken){
if (
this.authenticated &&
keycloak.token &&
keycloak.idTokenParsed &&
keycloak.refreshToken
) {
this.user.username = keycloak.idTokenParsed.given_name;
this.user.token = keycloak.token;
this.user.refreshToken = keycloak.refreshToken;
}
},
async login(){
async login() {
try {
const keycloak = await keycloakService.callLogin();
if (keycloak)
await this.initOauth(keycloak);
if (keycloak) await this.initOauth(keycloak);
} catch (error) {
console.log(error)
console.log(error);
}
},
async signup() {
try {
const keycloak = await keycloakService.callSignup();
if (keycloak)
await this.initOauth(keycloak);
if (keycloak) await this.initOauth(keycloak);
} catch (error) {
console.log(error)
console.log(error);
}
},
// Logout user
async logout() {
try {
await keycloakService.CallLogout(import.meta.env.VITE_APP_URL + "/test");
await keycloakService.CallLogout(
import.meta.env.VITE_APP_URL + "/test"
);
await this.clearUserData();
} catch (error) {
console.error(error);
@ -58,15 +65,11 @@ export const useAuthStore = defineStore("storeAuth", {
async refreshUserToken() {
try {
const keycloak = await keycloakService.CallTokenRefresh();
if (keycloak)
await this.initOauth(keycloak, false);
if (keycloak) await this.initOauth(keycloak, false);
} catch (error) {
console.error(error);
}
},
test() {
this.testv = !this.testv;
},
// Clear user's store data
clearUserData() {
this.authenticated = false;
@ -75,6 +78,10 @@ export const useAuthStore = defineStore("storeAuth", {
refreshToken: "",
username: "",
};
}
}
},
},
});
type AuthStore = ReturnType<typeof useAuthStore>;
export { useAuthStore, type AuthStore };

View File

@ -1,15 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>

View File

@ -1,22 +1,25 @@
<script setup lang="ts">
import {errorList} from "@/services/popupDisplayer.ts";
import { errorList } from "@/services/popupDisplayer.ts";
import ErrorModal from "@/components/errorModal.vue";
</script>
<template>
<div class="error-wrapper">
<error-modal v-for="elm in errorList" :data=elm></error-modal>
</div>
<div class="error-wrapper">
<error-modal
v-for="elm in errorList"
:key="elm.id"
:error-message="elm.message"
:error-color="elm.color"
/>
</div>
</template>
<style scoped>
.error-wrapper{
.error-wrapper {
position: absolute;
left: 70%;
//background-color: blue;
height: 100%;
width: 30%;
}
</style>

View File

@ -1,75 +0,0 @@
<script setup lang="ts">
import {store} from "../main.ts";
import {callApi} from "@/services/api.ts";
import ErrorModal from "@/components/errorModal.vue";
import {errorList} from "@/services/popupDisplayer.ts";
import TempModal from "@/components/temp-modal.vue";
import ErrorWrapper from "@/App.vue";
function addResToTable(id: any){
return (req: any) => {
console.log(req)
}
}
</script>
<template>
<h1>Test page</h1>
<table class="test" style="width:100%">
<tbody>
<tr>
<td>Is Currently Authenticated ? </td>
<td>{{store.authenticated}}</td>
<td><button @click="store.login">Login</button></td>
<td><button @click="store.logout">Logout</button></td>
<td><button @click="store.signup">Signup</button></td>
</tr>
<tr>
<td>current token</td>
<td>{{store.user.token}}</td>
<td><button @click="store.refreshUserToken">Refresh</button></td>
</tr>
<tr>
<td>Current refresh token</td>
<td>{{store.user.refreshToken}}</td>
</tr>
<tr>
<td>Entrepreneur API call</td>
<td><button @click="callApi('random')">call</button></td>
<td>res</td>
<td></td>
</tr>
<tr>
<td>Admin API call</td>
<td><button @click="callApi('random2')">call</button></td>
<td>res</td>
<td></td>
</tr>
<tr>
<td>Unauth API call</td>
<td><button @click="callApi('random3')">call</button></td>
<td>res</td>
<td id="3"></td>
</tr>
</tbody>
</table>
<temp-modal></temp-modal>
</template>
<style scoped>
table {
width: 100px;
table-layout: fixed
}
tr {
width: 100%;
height: 100%;
}
td {
border: solid 1px black;
width: 20%;
height: 100%;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,90 @@
<script lang="ts" setup>
import { store } from "../main.ts";
import { callApi } from "@/services/api.ts";
import { ref } from "vue";
const CustomRequest = ref("");
</script>
<template>
<h1>Test page</h1>
<table class="test" style="width: 100%">
<tbody>
<tr>
<td>Is Currently Authenticated ?</td>
<td>{{ store.authenticated }}</td>
<td>
<button @click="store.login">Login</button>
</td>
<td>
<button @click="store.logout">Logout</button>
</td>
<td>
<button @click="store.signup">Signup</button>
</td>
</tr>
<tr>
<td>current token</td>
<td>{{ store.user.token }}</td>
<td>
<button @click="store.refreshUserToken">Refresh</button>
</td>
</tr>
<tr>
<td>Current refresh token</td>
<td>{{ store.user.refreshToken }}</td>
</tr>
<tr>
<td>Entrepreneur API call</td>
<td>
<button @click="callApi('random')">call</button>
</td>
<td>res</td>
<td></td>
</tr>
<tr>
<td>Admin API call</td>
<td>
<button @click="callApi('random2')">call</button>
</td>
<td>res</td>
<td></td>
</tr>
<tr>
<td>Unauth API call</td>
<td>
<button @click="callApi('unauth/dev')">call</button>
</td>
<td>res</td>
<td id="3"></td>
</tr>
<tr>
<td>
<input v-model="CustomRequest" placeholder="edit me" />
</td>
<td>
<button @click="callApi(CustomRequest)">call</button>
</td>
</tr>
</tbody>
</table>
</template>
<style scoped>
table {
width: 100px;
table-layout: fixed;
}
tr {
width: 100%;
height: 100%;
}
td {
border: solid 1px black;
width: 20%;
height: 100%;
overflow: hidden;
}
</style>

2
hooks/README.md Normal file
View File

@ -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`

24
hooks/google-java-format Executable file
View File

@ -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

5
postgres/Dockerfile Normal file
View File

@ -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

17
postgres/create_db.sh Normal file
View File

@ -0,0 +1,17 @@
#!/bin/bash
set -e
POSTGRES="psql --username ${POSTGRES_USER}"
echo "Creating database: ${DB_NAME}"
$POSTGRES <<EOSQL
CREATE DATABASE ${BACKEND_DB} OWNER ${BACKEND_USER};
EOSQL
echo "Creating database: ${DB_NAME}"
$POSTGRES <<EOSQL
CREATE DATABASE ${KEYCLOAK_DB} OWNER ${KEYCLOAK_USER};
EOSQL

16
postgres/create_user.sh Normal file
View File

@ -0,0 +1,16 @@
#!/bin/bash
set -e
POSTGRES="psql --username ${POSTGRES_USER}"
echo "Creating database role: [${BACKEND_USER}]"
$POSTGRES <<-EOSQL
CREATE USER ${BACKEND_USER} WITH CREATEDB PASSWORD '${BACKEND_PASSWORD}';
EOSQL
echo "Creating database role: ${KEYCLOAK_USER}"
$POSTGRES <<-EOSQL
CREATE USER ${KEYCLOAK_USER} WITH CREATEDB PASSWORD '${KEYCLOAK_PASSWORD}';
EOSQL