Compare commits

...

26 Commits

Author SHA1 Message Date
dfea20b9c4 fix: aadded changes, doc is ssomewhat coherent still need to change some endpoint names in controller and some minor changes
All checks were successful
Format / formatting (push) Successful in 6s
Build / build (push) Successful in 38s
CI / build (push) Successful in 10s
2025-04-16 12:31:00 +02:00
Pierre Tellier
f96872fb6b fix: not red anymore
All checks were successful
Format / formatting (push) Successful in 6s
Build / build (push) Successful in 38s
CI / build (push) Successful in 11s
2025-04-16 11:55:54 +02:00
Pierre Tellier
0140672812 why is cache enabled again ??
Some checks failed
Format / formatting (push) Successful in 5s
Build / build (push) Failing after 38s
CI / build (push) Successful in 11s
2025-04-16 11:29:25 +02:00
Théo Le Lez
7df2c768c8 fix : tests
Some checks failed
CI / build (push) Waiting to run
Format / formatting (push) Successful in 5s
Build / build (push) Has been cancelled
2025-04-16 11:26:25 +02:00
Théo Le Lez
6029457735 fix: more tests working, still need fixes
Some checks failed
Format / formatting (push) Failing after 4s
Build / build (push) Failing after 5m8s
CI / build (push) Successful in 10s
2025-04-16 10:36:59 +02:00
55112c8508 test: added initial test file mainly definitions and descritions of tests haven't finshed
Some checks failed
Format / formatting (push) Failing after 6s
Build / build (push) Failing after 5m6s
CI / build (push) Successful in 11s
2025-04-16 10:15:13 +02:00
676f1204cb feat: updated doc to reflect details of server, still not done editing it
Some checks failed
CI / build (push) Waiting to run
Format / formatting (push) Successful in 7s
Build / build (push) Has been cancelled
2025-04-16 10:12:16 +02:00
Pierre Tellier
66be0baca6 feat: created a better account creation flow
All checks were successful
Format / formatting (push) Successful in 6s
Build / build (push) Successful in 5m20s
CI / build (push) Successful in 11s
2025-04-09 17:39:43 +02:00
Pierre Tellier
385c5cd8d0 fix: added back the cache since the action ran once on the main branch
All checks were successful
Format / formatting (push) Successful in 5s
Build / build (push) Successful in 5m16s
CI / build (push) Successful in 12s
2025-04-06 20:32:45 +02:00
Pierre Tellier
b672dd200c fix: name coherence + test logic
All checks were successful
Format / formatting (push) Successful in 6s
Build / build (push) Successful in 43s
CI / build (push) Successful in 13s
2025-04-06 20:30:29 +02:00
Pierre Tellier
9e1f568ea4 fix: comparaison between two projects instead of their IDs
Some checks failed
Format / formatting (push) Successful in 6s
Build / build (push) Failing after 38s
CI / build (push) Successful in 14s
2025-04-06 20:14:43 +02:00
Théo Le Lez
aaa6e46d0c fix (kinda) : refactored update of data, still trying to fix bug
Some checks failed
Format / formatting (push) Successful in 6s
Build / build (push) Failing after 39s
CI / build (push) Successful in 11s
from EntrepreneurApiServiceTests (code is a bit messy with prints and comments dw)
2025-04-06 19:22:18 +02:00
Maillal
9b9cfbdb2e merged openapi with backend-test
Some checks failed
Format / formatting (push) Successful in 6s
Build / build (push) Failing after 37s
CI / build (push) Successful in 13s
2025-04-02 11:17:28 +02:00
Théo Le Lez
5b6b647697 feat: started test for EntrepreneurServiceApi
Some checks failed
Format / formatting (push) Successful in 5s
Build / build (push) Failing after 37s
CI / build (push) Successful in 11s
2025-04-02 10:25:50 +02:00
Maillal
81ce4fdb4c fix: commented docker back lines in order to push after trying to fix the error in tests
All checks were successful
Format / formatting (push) Successful in 6s
Build / build (push) Successful in 39s
CI / build (push) Successful in 11s
2025-03-30 19:35:14 +02:00
Maillal
ebd76a30ee feat: updated date format next is descriptions and setting up http request tests 2025-03-27 17:34:41 +01:00
Maillal
6ff6ce5052 fix: just fixed duplicate test files due to bad merge
All checks were successful
Format / formatting (push) Successful in 9s
Build / build (push) Successful in 43s
CI / build (push) Successful in 11s
2025-03-26 10:04:46 +01:00
60ec920cff fix: split openapi src files and made bash file to run
Some checks failed
Format / formatting (push) Successful in 5s
Build / build (push) Failing after 32s
CI / build (push) Successful in 11s
2025-03-23 17:12:31 +01:00
900a4c5bdc merged backend-api onto openapi to avoid working with worktree
Some checks failed
Format / formatting (push) Successful in 7s
Build / build (push) Failing after 30s
CI / build (push) Successful in 11s
2025-03-23 15:41:44 +01:00
d0b615c59d fix: the endpoints reflect the db models and changes since the last version of the doc, notes.md indicate some changes that are needed to happen in the controller and questions to be discussed
All checks were successful
CI / build (push) Successful in 11s
2025-03-21 09:42:57 +01:00
eccf116f49 feat: added swagger-ui to read docs and test api, fix: minor changes to yaml file, more to come
All checks were successful
CI / build (push) Successful in 13s
2025-03-20 19:11:32 +01:00
8491c9b3cf added minor change 2025-03-19 22:02:28 +01:00
067eeb9494 docs: added examples and changed some weird character
All checks were successful
CI / build (push) Successful in 11s
2025-02-26 11:31:45 +01:00
b355463dd9 docs: added needed modications and reorganized yaml file
All checks were successful
CI / build (push) Successful in 11s
2025-02-25 22:34:33 +01:00
79e949bdd4 feat: configured gradle file to generate code for api interface from swagger specification
All checks were successful
CI / build (push) Successful in 11s
2025-02-19 01:36:52 +01:00
ef8c8e896d feat: added openapi documentation to the project
All checks were successful
CI / build (push) Successful in 11s
2025-02-12 20:59:13 +01:00
41 changed files with 5413 additions and 136 deletions

View File

@ -27,7 +27,6 @@ jobs:
uses: gradle/actions/setup-gradle@v4
with:
cache-disabled: true # Once the code has been pushed once in main, this should be reenabled.
- name: init gradle
working-directory: ./MyINPulse-back/
run: ./gradlew build # todo: run test, currently fail because no database is present

3
.gitignore vendored
View File

@ -1,5 +1,8 @@
.env
.idea
keycloak/CAS/target
keycloak/.installed
docker-compose.yaml
node_modules
.vscode
postgres/data

View File

@ -61,8 +61,6 @@ public class WebSecurityCustomConfiguration {
.requestMatchers("/admin/**", "/shared/**")
.access(hasRole("REALM_MyINPulse-admin"))
.requestMatchers("/unauth/**")
.permitAll()
.anyRequest()
.authenticated())
.oauth2ResourceServer(
oauth2 ->

View File

@ -99,4 +99,20 @@ public class AdminApi {
public void deleteProject(@PathVariable long projectId) {
adminApiService.deleteProject(projectId);
}
@GetMapping("/admin/setadmin/{userId}")
public void setAdmin(@PathVariable long userId, @AuthenticationPrincipal Jwt principal) {
this.adminApiService.setAdmin(userId, principal.getTokenValue());
}
@GetMapping("/admin/validate_user_account/{userId}")
public void validateEntrepreneurAcc(
@PathVariable long userId, @AuthenticationPrincipal Jwt principal) {
this.adminApiService.validateEntrepreneurAccount(userId, principal.getTokenValue());
}
@GetMapping("/admin/get_pending_accounts")
public Iterable<User> validateEntrepreneurAcc() {
return this.adminApiService.getPendingUsers();
}
}

View File

@ -1,7 +1,7 @@
package enseirb.myinpulse.controller;
import enseirb.myinpulse.model.SectionCell;
import enseirb.myinpulse.model.Project;
import enseirb.myinpulse.model.SectionCell;
import enseirb.myinpulse.service.EntrepreneurApiService;
import org.springframework.beans.factory.annotation.Autowired;
@ -31,10 +31,10 @@ public class EntrepreneurApi {
@PutMapping("/entrepreneur/lcsection/modify/{sectionId}")
public void editSectionCell(
@PathVariable Long sectionId,
@RequestBody SectionCell sectionCell,
@RequestBody String content,
@AuthenticationPrincipal Jwt principal) {
entrepreneurApiService.editSectionCell(
sectionId, sectionCell, principal.getClaimAsString("email"));
sectionId, content, principal.getClaimAsString("email"));
}
/**

View File

@ -0,0 +1,51 @@
package enseirb.myinpulse.controller;
import enseirb.myinpulse.model.Entrepreneur;
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 UnauthApi {
private final EntrepreneurApiService entrepreneurApiService;
@Autowired
UnauthApi(EntrepreneurApiService entrepreneurApiService) {
this.entrepreneurApiService = entrepreneurApiService;
}
@GetMapping("/unauth/create_account")
public void createAccount(@AuthenticationPrincipal Jwt principal) {
boolean sneeStatus;
if (principal.getClaimAsString("sneeStatus") != null) {
sneeStatus = principal.getClaimAsString("sneeStatus").equals("true");
} else {
sneeStatus = false;
}
String userSurname = principal.getClaimAsString("userSurname");
String username = principal.getClaimAsString("preferred_username");
String primaryMail = principal.getClaimAsString("email");
String secondaryMail = principal.getClaimAsString("secondaryMail");
String phoneNumber = principal.getClaimAsString("phoneNumber");
String school = principal.getClaimAsString("school");
String course = principal.getClaimAsString("course");
Entrepreneur e =
new Entrepreneur(
userSurname,
username,
primaryMail,
secondaryMail,
phoneNumber,
school,
course,
sneeStatus,
true);
entrepreneurApiService.createAccount(e);
}
}

View File

@ -37,7 +37,7 @@ public class Administrator extends User {
String primaryMail,
String secondaryMail,
String phoneNumber) {
super(null, userSurname, username, primaryMail, secondaryMail, phoneNumber);
super(userSurname, username, primaryMail, secondaryMail, phoneNumber, false);
}
public List<Project> getListProject() {

View File

@ -44,15 +44,30 @@ public class Entrepreneur extends User {
String phoneNumber,
String school,
String course,
boolean sneeStatus) {
super(userSurname, username, primaryMail, secondaryMail, phoneNumber);
boolean sneeStatus,
boolean pending) {
super(userSurname, username, primaryMail, secondaryMail, phoneNumber, pending);
this.school = school;
this.course = course;
this.sneeStatus = sneeStatus;
}
public Entrepreneur(
String userSurname,
String username,
String primaryMail,
String secondaryMail,
String phoneNumber,
String school,
String course,
boolean sneeStatus) {
super(userSurname, username, primaryMail, secondaryMail, phoneNumber, false);
this.school = school;
this.course = course;
this.sneeStatus = sneeStatus;
}
public Entrepreneur(
Long idUser,
String userSurname,
String userName,
String primaryMail,
@ -63,8 +78,9 @@ public class Entrepreneur extends User {
boolean sneeStatus,
Project projectParticipation,
Project projectProposed,
MakeAppointment makeAppointment) {
super(idUser, userSurname, userName, primaryMail, secondaryMail, phoneNumber);
MakeAppointment makeAppointment,
boolean pending) {
super(userSurname, userName, primaryMail, secondaryMail, phoneNumber, pending);
this.school = school;
this.course = course;
this.sneeStatus = sneeStatus;

View File

@ -66,6 +66,15 @@ public class Project {
this.entrepreneurProposed = entrepreneurProposed;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
Project project = (Project) o;
return this.idProject == project.idProject;
}
public Long getIdProject() {
return idProject;
}

View File

@ -2,6 +2,9 @@ package enseirb.myinpulse.model;
import jakarta.persistence.*;
import org.hibernate.annotations.Generated;
import org.hibernate.generator.EventType;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@ -20,6 +23,10 @@ public class SectionCell {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idSectionCell;
@Column(columnDefinition = "serial")
@Generated(event = EventType.INSERT)
private Long idReference;
@Column() private long sectionId;
private String contentSectionCell;
@ -56,6 +63,14 @@ public class SectionCell {
this.idSectionCell = idSectionCell;
}
public Long getIdReference() {
return idReference;
}
public void setIdReference(Long idReference) {
this.idReference = idReference;
}
public Long getSectionId() {
return sectionId;
}

View File

@ -26,36 +26,23 @@ public class User {
@Column(length = 20)
private String phoneNumber;
@Column private boolean pending;
public User() {}
// TODO: this should be removed as we shouldn't be able to chose the ID. Leaving it for
// compatibility purposes, as soon as it's not used anymore, delete it
public User(
Long idUser,
String userSurname,
String userName,
String primaryMail,
String secondaryMail,
String phoneNumber) {
this.idUser = idUser;
this.userSurname = userSurname;
this.userName = userName;
this.primaryMail = primaryMail;
this.secondaryMail = secondaryMail;
this.phoneNumber = phoneNumber;
}
public User(
String userSurname,
String userName,
String primaryMail,
String secondaryMail,
String phoneNumber) {
String phoneNumber,
boolean pending) {
this.userSurname = userSurname;
this.userName = userName;
this.primaryMail = primaryMail;
this.secondaryMail = secondaryMail;
this.phoneNumber = phoneNumber;
this.pending = pending;
}
public Long getIdUser() {
@ -105,4 +92,12 @@ public class User {
public void setPhoneNumber(String phoneNumber) {
phoneNumber = phoneNumber;
}
public boolean isPending() {
return pending;
}
public void setPending(boolean pending) {
this.pending = pending;
}
}

View File

@ -9,8 +9,9 @@ import java.util.Optional;
@RepositoryRestResource
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByPrimaryMail(String email);
Optional<User> findByPrimaryMail(String primaryMail);
Iterable<User> findAllByPendingEquals(boolean pending);
/* @Query("SELECT u from User u")
User findAllUser(); */

View File

@ -24,6 +24,7 @@ public class AdminApiService {
private final ProjectService projectService;
private final UserService userService;
private final AdministratorService administratorService;
private final EntrepreneurService entrepreneurService;
private final UtilsService utilsService;
private final AppointmentService appointmentService;
private final ReportService reportService;
@ -35,6 +36,7 @@ public class AdminApiService {
UserService userService,
AdministratorService administratorService,
UtilsService utilsService,
EntrepreneurService entrepreneurService,
AppointmentService appointmentService,
ReportService reportService,
SectionCellService sectionCellService) {
@ -45,6 +47,7 @@ public class AdminApiService {
this.appointmentService = appointmentService;
this.reportService = reportService;
this.sectionCellService = sectionCellService;
this.entrepreneurService = entrepreneurService;
}
// TODO: check if tests are sufficient - peer verification required
@ -75,6 +78,12 @@ public class AdminApiService {
}
if (user instanceof Entrepreneur) {
Project project = ((Entrepreneur) user).getProjectParticipation();
if (project == null) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND,
"The user has no project, thus no appointments. No users should have no project");
}
project.getListSectionCell()
.forEach(
sectionCell -> {
@ -97,13 +106,14 @@ public class AdminApiService {
decision.projectId,
null,
null,
null,
(decision.isAccepted == 1) ? ACTIVE : REJECTED,
null,
null,
this.administratorService.getAdministratorById(decision.adminId));
}
// TODO: check if tests are sufficient - peer verification required
public void addNewProject(Project project) {
public Project addNewProject(Project project) {
project.setIdProject(null);
// We remove the ID from the request to be sure that it will be auto generated
try {
@ -135,6 +145,7 @@ public class AdminApiService {
sectionCell -> {
sectionCell.setProjectSectionCell(newProject);
});
return newProject;
}
public void createAppointmentReport(long appointmentId, Report report, String mail) {
@ -163,4 +174,36 @@ public class AdminApiService {
public void deleteProject(long projectId) {
this.projectService.deleteProjectById(projectId);
}
public void setAdmin(long userId, String token) {
Entrepreneur e = this.entrepreneurService.getEntrepreneurById(userId);
Administrator a =
new Administrator(
e.getUserSurname(),
e.getUserName(),
e.getPrimaryMail(),
e.getSecondaryMail(),
e.getPhoneNumber());
this.entrepreneurService.deleteEntrepreneur(e);
this.administratorService.addAdministrator(a);
try {
KeycloakApi.setRoleToUser(a.getUserName(), "MyINPulse-admin", token);
} catch (Exception err) {
logger.error(err);
}
}
public void validateEntrepreneurAccount(long userId, String token) {
Entrepreneur e = this.entrepreneurService.getEntrepreneurById(userId);
try {
KeycloakApi.setRoleToUser(e.getUserName(), "MyINPulse-entrepreneur", token);
} catch (Exception err) {
logger.error(err);
}
this.entrepreneurService.validateEntrepreneurById(userId);
}
public Iterable<User> getPendingUsers() {
return this.userService.getPendingAccounts();
}
}

View File

@ -2,10 +2,13 @@ package enseirb.myinpulse.service;
import static enseirb.myinpulse.model.ProjectDecisionValue.PENDING;
import enseirb.myinpulse.model.Entrepreneur;
import enseirb.myinpulse.model.Project;
import enseirb.myinpulse.model.SectionCell;
import enseirb.myinpulse.service.database.EntrepreneurService;
import enseirb.myinpulse.service.database.ProjectService;
import enseirb.myinpulse.service.database.SectionCellService;
import enseirb.myinpulse.service.database.UserService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -14,6 +17,8 @@ import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
@Service
public class EntrepreneurApiService {
@ -22,20 +27,26 @@ public class EntrepreneurApiService {
private final SectionCellService sectionCellService;
private final ProjectService projectService;
private final UtilsService utilsService;
private final UserService userService;
private final EntrepreneurService entrepreneurService;
@Autowired
EntrepreneurApiService(
SectionCellService sectionCellService,
ProjectService projectService,
UtilsService utilsService) {
UtilsService utilsService,
UserService userService,
EntrepreneurService entrepreneurService) {
this.sectionCellService = sectionCellService;
this.projectService = projectService;
this.utilsService = utilsService;
this.userService = userService;
this.entrepreneurService = entrepreneurService;
}
public void editSectionCell(Long sectionCellId, SectionCell sectionCell, String mail) {
SectionCell editSectionCell = sectionCellService.getSectionCellById(sectionCellId);
if (editSectionCell == null) {
public void editSectionCell(Long sectionCellId, String content, String mail) {
SectionCell sectionCell = sectionCellService.getSectionCellById(sectionCellId);
if (sectionCell == null) {
System.err.println("Trying to edit unknown section cell");
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "Cette cellule de section n'existe pas");
@ -55,11 +66,28 @@ public class EntrepreneurApiService {
mail,
sectionCellId,
this.sectionCellService.getProjectId(sectionCellId));
sectionCellService.updateSectionCell(
sectionCellId,
sectionCell.getSectionId(),
sectionCell.getContentSectionCell(),
sectionCell.getModificationDate());
SectionCell newSectionCell =
new SectionCell(
null,
sectionCell.getSectionId(),
content,
LocalDateTime.now(),
sectionCell.getProjectSectionCell());
newSectionCell.setIdReference(sectionCell.getIdReference());
sectionCell
.getAppointmentSectionCell()
.forEach(
appointment -> {
newSectionCell.updateAppointmentSectionCell(appointment);
});
sectionCell
.getListAnnotation()
.forEach(
annotation -> {
newSectionCell.updateListAnnotation(annotation);
});
this.addSectionCell(newSectionCell, mail);
// sectionCellService.updateSectionCell(sectionCellId, content, null, null, null);
}
public void removeSectionCell(Long sectionCellId, String mail) {
@ -75,7 +103,7 @@ public class EntrepreneurApiService {
"User {} tried to remove section cells {} of the project {} but is not allowed to.",
mail,
sectionCellId,
this.sectionCellService.getSectionCellById(sectionCellId));
this.sectionCellService.getProjectId(sectionCellId));
throw new ResponseStatusException(
HttpStatus.UNAUTHORIZED, "You're not allowed to check this project");
}
@ -84,7 +112,20 @@ public class EntrepreneurApiService {
mail,
sectionCellId,
this.sectionCellService.getProjectId(sectionCellId));
sectionCellService.removeSectionCellById(sectionCellId);
SectionCell removedSectionCell =
new SectionCell(
null,
-1L,
"",
LocalDateTime.now(),
this.projectService.getProjectById(
editSectionCell.getProjectSectionCell().getIdProject()));
sectionCellService.addNewSectionCell(removedSectionCell);
this.sectionCellService.updateSectionCellReferenceId(
removedSectionCell.getIdSectionCell(), editSectionCell.getIdReference());
projectService.updateProjectListSectionCell(
sectionCellService.getProjectId(sectionCellId), removedSectionCell);
// sectionCellService.removeSectionCellById(sectionCellId);
}
public void addSectionCell(SectionCell sectionCell, String mail) {
@ -93,21 +134,28 @@ public class EntrepreneurApiService {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST, "La cellule de section fournie est vide");
}
if (sectionCell.getSectionId() == -1) {
System.err.println("Trying to create an illegal section cell");
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST, "La cellule de section fournie n'est pas valide");
}
if (!utilsService.isAllowedToCheckProject(
mail, this.sectionCellService.getProjectId(sectionCell.getIdSectionCell()))) {
mail, sectionCell.getProjectSectionCell().getIdProject())) {
logger.warn(
"User {} tried to add a section cell to the project {} but is not allowed to.",
mail,
this.sectionCellService.getProjectId(sectionCell.getIdSectionCell()));
sectionCell.getProjectSectionCell().getIdProject());
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 {}",
"User {} added a new section cell {} to the project {}",
mail,
sectionCell.getIdSectionCell(),
this.sectionCellService.getProjectId(sectionCell.getIdSectionCell()));
SectionCell newSectionCell = sectionCellService.addNewSectionCell(sectionCell);
sectionCell.getProjectSectionCell().getIdProject());
SectionCell newSectionCell =
sectionCellService.addNewSectionCell(
sectionCell); // if here, logger fails cause id is null (not added yet)
newSectionCell.getProjectSectionCell().updateListSectionCell(newSectionCell);
newSectionCell
.getAppointmentSectionCell()
@ -130,6 +178,31 @@ public class EntrepreneurApiService {
}
logger.info("User {} created a new project with id {}", mail, project.getIdProject());
project.setProjectStatus(PENDING);
project.setEntrepreneurProposed((Entrepreneur) this.userService.getUserByEmail(mail));
projectService.addNewProject(project);
project.getProjectAdministrator().updateListProject(project);
this.entrepreneurService.updateEntrepreneurProjectProposed(
this.userService.getUserByEmail(mail).getIdUser(), project);
project.getListEntrepreneurParticipation()
.forEach(
entrepreneur ->
this.entrepreneurService.updateEntrepreneurProjectParticipation(
entrepreneur.getIdUser(), project));
project.getListSectionCell()
.forEach(
sectionCell ->
this.sectionCellService.updateSectionCellProject(
sectionCell.getIdSectionCell(), project));
}
public void createAccount(Entrepreneur e) {
try {
userService.getUserByEmail(e.getPrimaryMail());
logger.error("The user {} already exists in the system", e.getPrimaryMail());
} catch (ResponseStatusException err) {
this.entrepreneurService.addEntrepreneur(e);
return;
}
throw new ResponseStatusException(HttpStatus.CONFLICT, "User already exists in the system");
}
}

View File

@ -6,12 +6,17 @@ import enseirb.myinpulse.exception.UserNotFoundException;
import enseirb.myinpulse.model.RoleRepresentation;
import enseirb.myinpulse.model.UserRepresentation;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.web.client.RestClient;
import java.util.List;
import javax.management.relation.RoleNotFoundException;
public class KeycloakApi {
protected static final Logger logger = LogManager.getLogger();
static final String keycloakUrl;
static final String realmName;
@ -29,44 +34,48 @@ public class KeycloakApi {
realmName = System.getenv("VITE_KEYCLOAK_REALM");
}
static String toBearer(String b) {
return "Bearer " + b;
}
/**
* 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
* @param token authorization header used by the client to authenticate to keycloak
*/
public static RoleRepresentation getRoleRepresentationByName(String roleName, String bearer)
public static RoleRepresentation getRoleRepresentationByName(String roleName, String token)
throws RoleNotFoundException {
RoleRepresentation[] response =
RoleRepresentation response =
RestClient.builder()
.baseUrl(keycloakUrl)
.defaultHeader("Authorization", bearer)
.defaultHeader("Authorization", toBearer(token))
.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];
.body(RoleRepresentation.class);
/*
{"id":"7a845f2e-c832-4465-8cd8-894d72bc13f1","name":"MyINPulse-entrepreneur","description":"Role for entrepreneur","composite":false,"clientRole":false,"containerId":"0d6f691b-e328-471a-b89e-c30bd7e5b6b0","attributes":{}}
*/
// TODO: check what happens when role does not exist
return response;
}
/**
* 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
* @param token bearer of the user, allowing access to database
* @return the userid, as a String
* @throws UserNotFoundException
*/
public static String getUserIdByName(String username, String bearer)
public static String getUserIdByName(String username, String token)
throws UserNotFoundException {
UserRepresentation[] response =
RestClient.builder()
.baseUrl(keycloakUrl)
.defaultHeader("Authorization", bearer)
.defaultHeader("Authorization", toBearer(token))
.build()
.get()
.uri(
@ -91,27 +100,26 @@ public class KeycloakApi {
*
* @param username
* @param roleName
* @param bearer
* @param token
* @throws RoleNotFoundException
* @throws UserNotFoundException
*/
public static void setRoleToUser(String username, String roleName, String bearer)
public static void setRoleToUser(String username, String roleName, String token)
throws RoleNotFoundException, UserNotFoundException {
RoleRepresentation roleRepresentation = getRoleRepresentationByName(roleName, bearer);
String userId = getUserIdByName(username, bearer);
RoleRepresentation roleRepresentation = getRoleRepresentationByName(roleName, token);
String userId = getUserIdByName(username, token);
List<RoleRepresentation> rolesToAdd = List.of(roleRepresentation);
logger.debug("Adding role {} to user {}", roleRepresentation.id, userId);
RestClient.builder()
.baseUrl(keycloakUrl)
.defaultHeader("Authorization", bearer)
.defaultHeader("Authorization", toBearer(token))
.build()
.post()
.uri(
"/admin/realms/${realmName}/users/${userId}/role-mappings/realm",
realmName,
userId)
.body(roleRepresentation)
.uri("/admin/realms/" + realmName + "/users/" + userId + "/role-mappings/realm")
.body(rolesToAdd)
.contentType(APPLICATION_JSON)
.retrieve();
.retrieve()
.toBodilessEntity();
}
/**

View File

@ -25,6 +25,7 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@Service
public class SharedApiService {
@ -73,6 +74,45 @@ public class SharedApiService {
project, sectionId, dateTime);
}
// Retrieve all up to date (for every sectionId) sectionCells of a project
public Iterable<SectionCell> getAllSectionCells(long projectId, 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");
}
Project project = this.projectService.getProjectById(projectId);
List<SectionCell> allSectionCells = new ArrayList<SectionCell>();
project.getListSectionCell()
.forEach(
projectCell -> {
AtomicBoolean sameReferenceId =
new AtomicBoolean(false); // side effect lambdas
allSectionCells.forEach(
selectedCell -> {
if (projectCell
.getIdReference()
.equals(selectedCell.getIdReference())) {
sameReferenceId.set(true);
if (projectCell
.getModificationDate()
.isAfter(selectedCell.getModificationDate())) {
allSectionCells.remove(selectedCell);
allSectionCells.add(projectCell);
}
}
});
if (!sameReferenceId.get()) {
allSectionCells.add(projectCell);
}
});
return allSectionCells;
}
// TODO: test
public Iterable<Entrepreneur> getEntrepreneursByProjectId(long projectId, String mail) {
if (!utilsService.isAllowedToCheckProject(mail, projectId)) {

View File

@ -15,6 +15,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.util.Objects;
@Service
public class UtilsService {
@ -44,8 +46,18 @@ public class UtilsService {
}
User user = this.userService.getUserByEmail(mail);
Entrepreneur entrepreneur = this.entrepreneurService.getEntrepreneurById(user.getIdUser());
if (entrepreneur == null) {
logger.debug("testing access with an unknown Entrepreneur");
return false;
}
if (entrepreneur.getProjectParticipation() == null) {
logger.debug("testing access with an user with no project participation");
return false;
}
Project project = this.projectService.getProjectById(projectId);
return entrepreneur.getProjectParticipation() == project;
// We compare the ID instead of the project themselves
return Objects.equals(
entrepreneur.getProjectParticipation().getIdProject(), project.getIdProject());
}
// TODO: test
@ -56,6 +68,7 @@ public class UtilsService {
return true;
} catch (ResponseStatusException e) {
logger.info(e);
return false;
}
}

View File

@ -1,6 +1,9 @@
package enseirb.myinpulse.service.database;
import enseirb.myinpulse.model.Administrator;
import enseirb.myinpulse.model.Annotation;
import enseirb.myinpulse.model.MakeAppointment;
import enseirb.myinpulse.model.Project;
import enseirb.myinpulse.repository.AdministratorRepository;
import org.apache.logging.log4j.LogManager;
@ -52,6 +55,49 @@ public class AdministratorService {
return this.administratorRepository.save(administrator);
}
public void updateAdministratorListProject(long idAdministrator, Project project) {
Administrator administrator = getAdministratorById(idAdministrator);
administrator.updateListProject(project);
this.administratorRepository.save(administrator);
}
public void updateAdministratorListAnnotation(long idAdministrator, Annotation annotation) {
Administrator administrator = getAdministratorById(idAdministrator);
administrator.updateListAnnotation(annotation);
this.administratorRepository.save(administrator);
}
public void updateAdministratorMakeAppointment(
long idAdministrator, MakeAppointment makeAppointment) {
Administrator administrator = getAdministratorById(idAdministrator);
administrator.setMakeAppointment(makeAppointment);
this.administratorRepository.save(administrator);
}
public Administrator updateAdministrator(
Long idAdministrator,
Project project,
Annotation annotation,
MakeAppointment makeAppointment) {
Optional<Administrator> administrator = administratorRepository.findById(idAdministrator);
if (administrator.isEmpty()) {
logger.error(
"updateAdministrator : No administrator found with id {}", idAdministrator);
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "Cet administrateur n'existe pas");
}
if (project != null) {
administrator.get().updateListProject(project);
}
if (annotation != null) {
administrator.get().updateListAnnotation(annotation);
}
if (makeAppointment != null) {
administrator.get().setMakeAppointment(makeAppointment);
}
return this.administratorRepository.save(administrator.get());
}
/*
public Administrator getAdministratorByProject(Project project) {
r

View File

@ -46,6 +46,12 @@ public class AnnotationService {
this.annotationRepository.deleteById(id);
}
public void updateAnnotationComment(long idAnnotation, String comment) {
Annotation annotation = getAnnotationById(idAnnotation);
annotation.setComment(comment);
this.annotationRepository.save(annotation);
}
public Annotation updateAnnotation(Long id, String comment) {
Optional<Annotation> annotation = annotationRepository.findById(id);
if (annotation.isEmpty()) {

View File

@ -1,6 +1,7 @@
package enseirb.myinpulse.service.database;
import enseirb.myinpulse.model.Appointment;
import enseirb.myinpulse.model.SectionCell;
import enseirb.myinpulse.repository.AppointmentRepository;
import org.apache.logging.log4j.LogManager;
@ -47,13 +48,50 @@ public class AppointmentService {
this.appointmentRepository.deleteById(id);
}
public void updateAppointmentDate(long idAppointment, LocalDate date) {
Appointment appointment = getAppointmentById(idAppointment);
appointment.setAppointmentDate(date);
this.appointmentRepository.save(appointment);
}
public void updateAppointmentTime(long idAppointment, LocalTime time) {
Appointment appointment = getAppointmentById(idAppointment);
appointment.setAppointmentTime(time);
this.appointmentRepository.save(appointment);
}
public void updateAppointmentDuration(long idAppointment, LocalTime duration) {
Appointment appointment = getAppointmentById(idAppointment);
appointment.setAppointmentDuration(duration);
this.appointmentRepository.save(appointment);
}
public void updateAppointmentPlace(long idAppointment, String place) {
Appointment appointment = getAppointmentById(idAppointment);
appointment.setAppointmentPlace(place);
this.appointmentRepository.save(appointment);
}
public void updateAppointmentSubject(long idAppointment, String subject) {
Appointment appointment = getAppointmentById(idAppointment);
appointment.setAppointmentSubject(subject);
this.appointmentRepository.save(appointment);
}
public void updateAppointmentListSectionCell(long idAppointment, SectionCell sectionCell) {
Appointment appointment = getAppointmentById(idAppointment);
appointment.updateListSectionCell(sectionCell);
this.appointmentRepository.save(appointment);
}
public Appointment updateAppointment(
Long id,
LocalDate appointmentDate,
LocalTime appointmentTime,
LocalTime appointmentDuration,
String appointmentPlace,
String appointmentSubject) {
String appointmentSubject,
SectionCell sectionCell) {
Optional<Appointment> appointment = this.appointmentRepository.findById(id);
if (appointment.isEmpty()) {
logger.error("updateAppointment : No appointment found with id {}", id);
@ -74,6 +112,9 @@ public class AppointmentService {
if (appointmentSubject != null) {
appointment.get().setAppointmentSubject(appointmentSubject);
}
if (sectionCell != null) {
appointment.get().updateListSectionCell(sectionCell);
}
return this.appointmentRepository.save(appointment.get());
}
}

View File

@ -1,6 +1,7 @@
package enseirb.myinpulse.service.database;
import enseirb.myinpulse.model.Entrepreneur;
import enseirb.myinpulse.model.MakeAppointment;
import enseirb.myinpulse.model.Project;
import enseirb.myinpulse.repository.EntrepreneurRepository;
@ -41,8 +42,52 @@ public class EntrepreneurService {
return this.entrepreneurRepository.save(entrepreneur);
}
public void updateEntrepreneurSchool(long idEntrepreneur, String school) {
Entrepreneur entrepreneur = getEntrepreneurById(idEntrepreneur);
entrepreneur.setSchool(school);
this.entrepreneurRepository.save(entrepreneur);
}
public void updateEntrepreneurCourse(long idEntrepreneur, String course) {
Entrepreneur entrepreneur = getEntrepreneurById(idEntrepreneur);
entrepreneur.setCourse(course);
this.entrepreneurRepository.save(entrepreneur);
}
public void updateEntrepreneurSneeStatus(long idEntrepreneur, boolean status) {
Entrepreneur entrepreneur = getEntrepreneurById(idEntrepreneur);
entrepreneur.setSneeStatus(status);
this.entrepreneurRepository.save(entrepreneur);
}
public void updateEntrepreneurProjectParticipation(
long idEntrepreneur, Project projectParticipation) {
Entrepreneur entrepreneur = getEntrepreneurById(idEntrepreneur);
entrepreneur.setProjectParticipation(projectParticipation);
this.entrepreneurRepository.save(entrepreneur);
}
public void updateEntrepreneurProjectProposed(long idEntrepreneur, Project projectProposed) {
Entrepreneur entrepreneur = getEntrepreneurById(idEntrepreneur);
entrepreneur.setProjectParticipation(projectProposed);
this.entrepreneurRepository.save(entrepreneur);
}
public void updateEntrepreneurMakeAppointment(
long idEntrepreneur, MakeAppointment makeAppointment) {
Entrepreneur entrepreneur = getEntrepreneurById(idEntrepreneur);
entrepreneur.setMakeAppointment(makeAppointment);
this.entrepreneurRepository.save(entrepreneur);
}
public Entrepreneur updateEntrepreneur(
Long id, String school, String course, Boolean sneeStatus) {
Long id,
String school,
String course,
Boolean sneeStatus,
Project projectParticipation,
Project projectProposed,
MakeAppointment makeAppointment) {
Optional<Entrepreneur> entrepreneur = entrepreneurRepository.findById(id);
if (entrepreneur.isEmpty()) {
logger.error("updateEntrepreneur : No entrepreneur found with id {}", id);
@ -58,10 +103,32 @@ public class EntrepreneurService {
if (sneeStatus != null) {
entrepreneur.get().setSneeStatus(sneeStatus);
}
if (projectParticipation != null) {
entrepreneur.get().setProjectParticipation(projectParticipation);
}
if (projectProposed != null) {
entrepreneur.get().setProjectParticipation(projectProposed);
}
if (makeAppointment != null) {
entrepreneur.get().setMakeAppointment(makeAppointment);
}
return this.entrepreneurRepository.save(entrepreneur.get());
}
public Iterable<Entrepreneur> GetEntrepreneurByProject(Project project) {
return this.entrepreneurRepository.getEntrepreneurByProjectParticipation(project);
}
public void deleteEntrepreneur(Entrepreneur e) {
this.entrepreneurRepository.delete(e);
}
public void validateEntrepreneurById(Long id) {
Optional<Entrepreneur> e = this.entrepreneurRepository.findById(id);
if (e.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Entrepreneur n'existe pas");
}
e.get().setPending(false);
this.entrepreneurRepository.save(e.get());
}
}

View File

@ -2,9 +2,7 @@ package enseirb.myinpulse.service.database;
import static enseirb.myinpulse.model.ProjectDecisionValue.PENDING;
import enseirb.myinpulse.model.Administrator;
import enseirb.myinpulse.model.Project;
import enseirb.myinpulse.model.ProjectDecisionValue;
import enseirb.myinpulse.model.*;
import enseirb.myinpulse.repository.ProjectRepository;
import org.apache.logging.log4j.LogManager;
@ -14,7 +12,6 @@ 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;
@ -52,12 +49,50 @@ public class ProjectService {
return this.projectRepository.save(project);
}
public void updateProjectName(long idProject, String name) {
Project project = getProjectById(idProject);
project.setProjectName(name);
this.projectRepository.save(project);
}
public void updateProjectLogo(long idProject, byte[] logo) {
Project project = getProjectById(idProject);
project.setLogo(logo);
this.projectRepository.save(project);
}
public void updateProjectStatus(long idProject, ProjectDecisionValue status) {
Project project = getProjectById(idProject);
project.setProjectStatus(status);
this.projectRepository.save(project);
}
public void updateProjectEntrepreneurParticipation(
long idProject, Entrepreneur entrepreneurParticipation) {
Project project = getProjectById(idProject);
project.updateListEntrepreneurParticipation(entrepreneurParticipation);
this.projectRepository.save(project);
}
public void updateProjectListSectionCell(long idProject, SectionCell sectionCell) {
Project project = getProjectById(idProject);
project.updateListSectionCell(sectionCell);
this.projectRepository.save(project);
}
public void updateProjectAdministrator(long idProject, Administrator administrator) {
Project project = getProjectById(idProject);
project.setProjectAdministrator(administrator);
this.projectRepository.save(project);
}
public Project updateProject(
Long id,
String projectName,
byte[] logo,
LocalDate creationDate,
ProjectDecisionValue projectStatus,
Entrepreneur entrepreneurParticipation,
SectionCell sectionCell,
Administrator administrator) {
Optional<Project> project = this.projectRepository.findById(id);
@ -73,11 +108,6 @@ public class ProjectService {
if (logo != null) {
project.get().setLogo(logo);
}
if (creationDate != null) {
project.get().setCreationDate(creationDate);
}
if (projectStatus != null) {
// TODO: check if this is really useful
/*
@ -89,7 +119,12 @@ public class ProjectService {
*/
project.get().setProjectStatus(projectStatus);
}
if (entrepreneurParticipation != null) {
project.get().updateListEntrepreneurParticipation(entrepreneurParticipation);
}
if (sectionCell != null) {
project.get().updateListSectionCell(sectionCell);
}
if (administrator != null) {
project.get().setProjectAdministrator(administrator);
}

View File

@ -1,5 +1,6 @@
package enseirb.myinpulse.service.database;
import enseirb.myinpulse.model.Appointment;
import enseirb.myinpulse.model.Report;
import enseirb.myinpulse.repository.ReportRepository;
@ -46,7 +47,19 @@ public class ReportService {
this.reportRepository.deleteById(id);
}
public Report updateReport(Long id, String reportContent) {
public void updateReportContent(long idReport, String content) {
Report report = getReportById(idReport);
report.setReportContent(content);
this.reportRepository.save(report);
}
public void updateReportAppointment(long idReport, Appointment appointment) {
Report report = getReportById(idReport);
report.setAppointmentReport(appointment);
this.reportRepository.save(report);
}
public Report updateReport(Long id, String reportContent, Appointment appointment) {
Optional<Report> report = this.reportRepository.findById(id);
if (report.isEmpty()) {
logger.error("updateReport : No report found with id {}", id);
@ -55,6 +68,9 @@ public class ReportService {
if (reportContent != null) {
report.get().setReportContent(reportContent);
}
if (appointment != null) {
report.get().setAppointmentReport(appointment);
}
return this.reportRepository.save(report.get());
}
}

View File

@ -1,5 +1,6 @@
package enseirb.myinpulse.service.database;
import enseirb.myinpulse.model.Annotation;
import enseirb.myinpulse.model.Appointment;
import enseirb.myinpulse.model.Project;
import enseirb.myinpulse.model.SectionCell;
@ -50,22 +51,63 @@ public class SectionCellService {
this.sectionCellRepository.deleteById(id);
}
public void updateSectionCellReferenceId(Long idSectionCell, Long referenceId) {
SectionCell sectionCell = this.getSectionCellById(idSectionCell);
sectionCell.setIdReference(referenceId);
this.sectionCellRepository.save(sectionCell);
}
public void updateSectionCellContent(long idSectionCell, String content) {
SectionCell sectionCell = getSectionCellById(idSectionCell);
sectionCell.setContentSectionCell(content);
this.sectionCellRepository.save(sectionCell);
}
public void updateSectionCellListAppointment(long idSectionCell, Appointment appointment) {
SectionCell sectionCell = getSectionCellById(idSectionCell);
sectionCell.updateAppointmentSectionCell(appointment);
this.sectionCellRepository.save(sectionCell);
}
public void updateSectionCellListAnnotation(long idSectionCell, Annotation annotation) {
SectionCell sectionCell = getSectionCellById(idSectionCell);
sectionCell.updateListAnnotation(annotation);
this.sectionCellRepository.save(sectionCell);
}
public void updateSectionCellProject(long idSectionCell, Project project) {
SectionCell sectionCell = getSectionCellById(idSectionCell);
sectionCell.setProjectSectionCell(project);
this.sectionCellRepository.save(sectionCell);
}
public SectionCell updateSectionCell(
Long id, Long sectionId, String contentSectionCell, LocalDateTime modificationDate) {
Long id,
String contentSectionCell,
Appointment appointment,
Annotation annotation,
Project project) {
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);
sectionCell.get().setModificationDate(LocalDateTime.now());
}
if (modificationDate != null) {
sectionCell.get().setModificationDate(modificationDate);
if (appointment != null) {
sectionCell.get().updateAppointmentSectionCell(appointment);
sectionCell.get().setModificationDate(LocalDateTime.now());
}
if (annotation != null) {
sectionCell.get().updateListAnnotation(annotation);
sectionCell.get().setModificationDate(LocalDateTime.now());
}
if (project != null) {
sectionCell.get().setProjectSectionCell(project);
sectionCell.get().setModificationDate(LocalDateTime.now());
}
return this.sectionCellRepository.save(sectionCell.get());
}

View File

@ -30,6 +30,15 @@ public class UserService {
return this.userRepository.findAll();
}
public User getUserById(long id) {
Optional<User> user = this.userRepository.findById(id);
if (user.isEmpty()) {
logger.error("getUserById : No user found with id {}", id);
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Cet utilisateur n'existe pas");
}
return user.get();
}
// TODO
public User getUserByEmail(String email) {
Optional<User> opt_user = this.userRepository.findByPrimaryMail(email);
@ -49,6 +58,36 @@ public class UserService {
return this.userRepository.save(user);
}
public void updateUserSurname(long idUser, String surname) {
User user = getUserById(idUser);
user.setUserSurname(surname);
this.userRepository.save(user);
}
public void updateUserName(long idUser, String name) {
User user = getUserById(idUser);
user.setUserName(name);
this.userRepository.save(user);
}
public void updateUserPrimaryMail(long idUser, String primaryMail) {
User user = getUserById(idUser);
user.setPrimaryMail(primaryMail);
this.userRepository.save(user);
}
public void updateUserSecondaryMail(long idUser, String secondaryMail) {
User user = getUserById(idUser);
user.setSecondaryMail(secondaryMail);
this.userRepository.save(user);
}
public void updateUserPhoneNumber(long idUser, String phoneNumber) {
User user = getUserById(idUser);
user.setPhoneNumber(phoneNumber);
this.userRepository.save(user);
}
public User updateUser(
@PathVariable Long id,
String userSurname,
@ -78,4 +117,8 @@ public class UserService {
}
return this.userRepository.save(user.get());
}
public Iterable<User> getPendingAccounts() {
return this.userRepository.findAllByPendingEquals(true);
}
}

View File

@ -4,12 +4,10 @@ import static enseirb.myinpulse.model.ProjectDecisionValue.*;
import static org.junit.jupiter.api.Assertions.*;
import enseirb.myinpulse.model.Administrator;
import enseirb.myinpulse.model.Entrepreneur;
import enseirb.myinpulse.model.Project;
import enseirb.myinpulse.model.ProjectDecision;
import enseirb.myinpulse.model.*;
import enseirb.myinpulse.service.AdminApiService;
import enseirb.myinpulse.service.database.AdministratorService;
import enseirb.myinpulse.service.database.AppointmentService;
import enseirb.myinpulse.service.database.EntrepreneurService;
import enseirb.myinpulse.service.database.ProjectService;
@ -21,6 +19,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
@ -37,7 +36,8 @@ public class AdminApiServiceTest {
static void setup(
@Autowired AdministratorService administratorService,
@Autowired ProjectService projectService,
@Autowired EntrepreneurService entrepreneurService) {
@Autowired EntrepreneurService entrepreneurService,
@Autowired AppointmentService appoitmentService) {
administratorService.addAdministrator(
new Administrator(
"admin",
@ -54,6 +54,7 @@ public class AdminApiServiceTest {
"testAdmin@example.com",
""));
administratorid = administrator.getIdUser();
entrepreneur =
new Entrepreneur(
"JeSuisUnEntrepreneurDeCompet",
@ -65,14 +66,33 @@ public class AdminApiServiceTest {
"info ofc",
false);
entrepreneurService.addEntrepreneur(entrepreneur);
projectService.addNewProject(
new Project(
"sampleProjectAdminApiService",
Entrepreneur entrepreneur2 =
new Entrepreneur(
"GDProjets", "", "Entrepreneur2@inpulse.com", "", "", "", "info ofc", true);
entrepreneurService.addEntrepreneur(entrepreneur2);
Project p =
projectService.addNewProject(
new Project(
"sampleProjectAdminApiService",
null,
LocalDate.now(),
ACTIVE,
administratorService.getAdministratorByPrimaryMain(
"testAdminFull@example.com")));
entrepreneurService.updateEntrepreneurProjectParticipation(entrepreneur2.getIdUser(), p);
Appointment a =
new Appointment(
null,
LocalDate.now(),
ACTIVE,
administratorService.getAdministratorByPrimaryMain(
"testAdminFull@example.com")));
LocalTime.now(),
LocalTime.now(),
"Salle TD 15",
"Discussion importante");
appoitmentService.addNewAppointment(a);
}
private <T> List<T> IterableToList(Iterable<T> iterable) {
@ -106,7 +126,7 @@ public class AdminApiServiceTest {
List<Project> l = IterableToList(projects);
assertEquals(1, l.size());
Project p = l.getFirst();
assertEquals(p.getProjectName(), "sampleProjectAdminApiService");
assertEquals("sampleProjectAdminApiService", p.getProjectName());
}
@Test
@ -180,7 +200,7 @@ public class AdminApiServiceTest {
@Test
void addProjectToAdmin() {
assertEquals(0, administrator.getListProject().size());
Project p1 = new Project("assProjectToAdmin", null, LocalDate.now(), ACTIVE, administrator);
Project p1 = new Project("addProjectToAdmin", null, LocalDate.now(), ACTIVE, administrator);
this.adminApiService.addNewProject(p1);
assertEquals(1, administrator.getListProject().size());
}
@ -189,7 +209,7 @@ public class AdminApiServiceTest {
void addProjectToUser() {
assertNull(entrepreneur.getProjectParticipation());
Project p1 =
new Project("assProjectToAdmin", null, LocalDate.now(), ACTIVE, null, entrepreneur);
new Project("addProjectToAdmin", null, LocalDate.now(), ACTIVE, null, entrepreneur);
this.adminApiService.addNewProject(p1);
assertEquals(p1, entrepreneur.getProjectParticipation());
}
@ -202,7 +222,7 @@ public class AdminApiServiceTest {
assertNull(e1.getProjectParticipation());
assertNull(e2.getProjectParticipation());
assertNull(e3.getProjectParticipation());
Project p1 = new Project("assProjectToAdmin", null, LocalDate.now(), ACTIVE, null, null);
Project p1 = new Project("addProjectToAdmin", null, LocalDate.now(), ACTIVE, null, null);
p1.updateListEntrepreneurParticipation(e1);
p1.updateListEntrepreneurParticipation(e2);
p1.updateListEntrepreneurParticipation(e3);
@ -221,4 +241,54 @@ public class AdminApiServiceTest {
this.adminApiService.addNewProject(p1);
assertThrows(ResponseStatusException.class, () -> this.adminApiService.addNewProject(p2));
}
// We could do a delete active project, but it's not really useful.
@Test
void deletePendingProject() {
int oldsize = IterableToList(this.adminApiService.getPendingProjects()).size();
Project p1 =
new Project("PendingProjectAdminApiService2", null, LocalDate.now(), PENDING, null);
Project p2 = this.adminApiService.addNewProject(p1);
assertEquals(oldsize + 1, IterableToList(this.adminApiService.getPendingProjects()).size());
this.adminApiService.deleteProject(p2.getIdProject());
assertEquals(oldsize, IterableToList(this.adminApiService.getPendingProjects()).size());
for (int i = 0; i < oldsize; i++) {
assertNotEquals(
p1.getIdProject(),
IterableToList(this.adminApiService.getPendingProjects())
.get(i)
.getIdProject());
}
}
@Test
void getUpcommingAppointmentUnkwnownUser() {
assertThrows(
ResponseStatusException.class,
() -> {
Iterable<Appointment> a =
this.adminApiService.getUpcomingAppointments(
"entrepreneur-inexistent@mail.fr");
});
}
@Test
void getUpcommingAppointmentNoProject() {
assertThrows(
ResponseStatusException.class,
() -> {
Iterable<Appointment> a =
this.adminApiService.getUpcomingAppointments(
"Entrepreneur@inpulse.com");
});
}
@Test
void getUpcommingAppointmentEmpty() {
Iterable<Appointment> a =
this.adminApiService.getUpcomingAppointments("Entrepreneur2@inpulse.com");
assertEquals(0, IterableToList(a).size());
}
}

View File

@ -0,0 +1,210 @@
package enseirb.myinpulse;
import static enseirb.myinpulse.model.ProjectDecisionValue.*;
import static org.junit.jupiter.api.Assertions.*;
import enseirb.myinpulse.model.Entrepreneur;
import enseirb.myinpulse.model.Project;
import enseirb.myinpulse.model.SectionCell;
import enseirb.myinpulse.service.EntrepreneurApiService;
import enseirb.myinpulse.service.database.EntrepreneurService;
import enseirb.myinpulse.service.database.ProjectService;
import enseirb.myinpulse.service.database.SectionCellService;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
@Transactional
public class EntrepreneurApiServiceTest {
private static Entrepreneur entrepreneur;
private static Project project;
private static Iterable<SectionCell> sectionCells2;
private static Iterable<SectionCell> sectionCells3;
@Autowired private EntrepreneurApiService entrepreneurApiService;
@Autowired private EntrepreneurService entrepreneurService;
@Autowired private ProjectService projectService;
@Autowired private SectionCellService sectionCellService;
@BeforeAll
static void setup(
@Autowired EntrepreneurService entrepreneurService,
@Autowired ProjectService projectService,
@Autowired SectionCellService sectionCellService) {
entrepreneur =
entrepreneurService.addEntrepreneur(
new Entrepreneur(
"entre",
"preneur",
"entrepreneur@mail.fr",
"entrepreneur2@mail.fr",
"01 45 71 25 48",
"ENSEIRB",
"Info",
false));
entrepreneurService.addEntrepreneur(
new Entrepreneur(
"entre2",
"preneur2",
"testentrepreneur@mail.fr",
"testentrepreneur2@mail.fr",
"",
"ENSEGID",
"",
true));
project =
projectService.addNewProject(
new Project("Project", null, LocalDate.now(), ACTIVE, null, entrepreneur));
entrepreneurService.updateEntrepreneurProjectProposed(entrepreneur.getIdUser(), project);
entrepreneurService.updateEntrepreneurProjectParticipation(
entrepreneur.getIdUser(), project);
SectionCell s1 =
sectionCellService.addNewSectionCell(
new SectionCell(
null,
2L,
"contenu très intéressant",
LocalDateTime.now(),
project));
SectionCell s2 =
sectionCellService.addNewSectionCell(
new SectionCell(
null,
3L,
"contenu très intéressant2",
LocalDateTime.now(),
project));
sectionCells2 = sectionCellService.getSectionCellsByProject(project, 2L);
sectionCells3 = sectionCellService.getSectionCellsByProject(project, 3L);
}
private <T> List<T> IterableToList(Iterable<T> iterable) {
List<T> l = new ArrayList<>();
iterable.forEach(l::add);
return l;
}
@Test
void editValidSectionCell() {
entrepreneurApiService.editSectionCell(
IterableToList(sectionCells2).getLast().getIdSectionCell(),
"modified content",
"entrepreneur@mail.fr");
// We get the data from the database again.
SectionCell s =
IterableToList(sectionCellService.getSectionCellsByProject(project, 2L)).getLast();
assertEquals("modified content", s.getContentSectionCell());
assertEquals(
2, IterableToList(sectionCellService.getSectionCellsByProject(project, 2L)).size());
}
@Test
void editInvalidSectionCell() {
assertThrows(
ResponseStatusException.class,
() ->
entrepreneurApiService.editSectionCell(
-1L, "should not be modified", "entrepreneur@mail.fr"));
SectionCell s =
IterableToList(sectionCellService.getSectionCellsByProject(project, 2L)).getLast();
assertEquals("contenu très intéressant", s.getContentSectionCell());
assertEquals(
1, IterableToList(sectionCellService.getSectionCellsByProject(project, 2L)).size());
}
@Test
void editSectionCellInvalidAccess() {
assertThrows(
ResponseStatusException.class,
() ->
entrepreneurApiService.editSectionCell(
IterableToList(sectionCells3).getFirst().getIdSectionCell(),
"should not be modified",
"testentrepreneur@mail.fr"));
SectionCell s =
IterableToList(sectionCellService.getSectionCellsByProject(project, 2L)).getFirst();
assertEquals("contenu très intéressant", s.getContentSectionCell());
}
@Test
void removeValidSectionCell() {
SectionCell tmpCell =
sectionCellService.addNewSectionCell(
new SectionCell(
null, 2L, "contenu temporaire", LocalDateTime.now(), project));
assertEquals(
2, IterableToList(sectionCellService.getSectionCellsByProject(project, 2L)).size());
assertDoesNotThrow(
() ->
entrepreneurApiService.removeSectionCell(
tmpCell.getIdSectionCell(), "entrepreneur@mail.fr"));
assertEquals(
tmpCell.getIdReference(),
IterableToList(sectionCellService.getSectionCellsByProject(project, -1L))
.getLast()
.getIdReference());
}
@Test
void removeInvalidSectionCell() {
assertThrows(
ResponseStatusException.class,
() -> entrepreneurApiService.removeSectionCell(-1L, "entrepreneur@mail.fr"));
SectionCell s =
IterableToList(sectionCellService.getSectionCellsByProject(project, 2L)).getFirst();
assertEquals("contenu très intéressant", s.getContentSectionCell());
}
@Test
void addValidSectionCell() {
SectionCell added =
sectionCellService.addNewSectionCell(
new SectionCell(null, 2L, "contenu ajouté", LocalDateTime.now(), project));
entrepreneurApiService.addSectionCell(added, "entrepreneur@mail.fr");
SectionCell s =
IterableToList(sectionCellService.getSectionCellsByProject(project, 2L)).getLast();
assertEquals("contenu ajouté", s.getContentSectionCell());
assertEquals(
2, IterableToList(sectionCellService.getSectionCellsByProject(project, 2L)).size());
}
@Test
void addSectionCellInvalidAccess() {
SectionCell added =
new SectionCell(null, 2L, "contenu ajouté", LocalDateTime.now(), project);
assertThrows(
ResponseStatusException.class,
() -> entrepreneurApiService.addSectionCell(added, "fauxentrepreneur@mail.fr"));
SectionCell s =
IterableToList(sectionCellService.getSectionCellsByProject(project, 2L)).getLast();
assertEquals(
1, IterableToList(sectionCellService.getSectionCellsByProject(project, 2L)).size());
assertEquals("contenu très intéressant", s.getContentSectionCell());
}
@Test
void addInvalidSectionCell() {
SectionCell added =
sectionCellService.addNewSectionCell(
new SectionCell(null, -1L, "contenu ajouté", LocalDateTime.now(), project));
assertThrows(
ResponseStatusException.class,
() -> entrepreneurApiService.addSectionCell(added, "entrepreneur@mail.fr"));
}
@Test
void requestValidProject() {}
}

View File

@ -0,0 +1,218 @@
package enseirb.myinpulse;
import static enseirb.myinpulse.model.ProjectDecisionValue.*;
import static org.junit.jupiter.api.Assertions.*;
import enseirb.myinpulse.model.Administrator;
import enseirb.myinpulse.model.Appointment;
import enseirb.myinpulse.model.Entrepreneur;
import enseirb.myinpulse.model.Project;
import enseirb.myinpulse.service.SharedApiService;
import enseirb.myinpulse.service.database.AdministratorService;
import enseirb.myinpulse.service.database.EntrepreneurService;
import enseirb.myinpulse.service.database.ProjectService;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
@Transactional
public class SharedApiServiceTest {
private static Administrator functional_administrator;
private static Entrepreneur functional_entrepreneur;
private static Project functional_project;
private static Entrepreneur empty_entrepreneur;
private static Administrator empty_administrator;
private static Project empty_Project;
@Autowired private SharedApiService sharedApiService;
@Autowired private ProjectService projectService;
@Autowired private AdministratorService adminService;
@Autowired private EntrepreneurService entrepreneurService;
private static Administrator getTestAdmin(String name) {
return new Administrator(
name, name, name + "@example.com", "seconday@example.com", "0123456789");
}
private static Entrepreneur getTestEntrpreneur(String name) {
return new Entrepreneur(
name,
name,
name + "@example.com",
"seconday@example.com",
"0123456789",
"School",
"Course",
false);
}
private static Project getTestProject(String name, Administrator admin) {
return new Project(name, null, LocalDate.now(), ACTIVE, admin);
}
static @BeforeAll private void setup(
@Autowired AdministratorService administratorService,
@Autowired ProjectService projectService,
@Autowired EntrepreneurService entrepreneurService) {
// empty_entrepreneur = entrepreneurService.addEntrepreneur(null);
// empty_administrator = administratorService.addAdministrator(null);
empty_Project = projectService.addNewProject(new Project());
functional_administrator =
administratorService.addAdministrator(getTestAdmin("functional_administrator"));
functional_entrepreneur =
entrepreneurService.addEntrepreneur(getTestEntrpreneur("functional_entrepreneur"));
functional_project =
projectService.addNewProject(
getTestProject("functional_project", functional_administrator));
functional_project.updateListEntrepreneurParticipation(functional_entrepreneur);
}
private <T> List<T> IterableToList(Iterable<T> iterable) {
List<T> l = new ArrayList<>();
iterable.forEach(l::add);
return l;
}
private <T> boolean matchesIgnoringId(T expected, T actual) {
if (expected instanceof Appointment e && actual instanceof Appointment a) {
return e.getAppointmentDate().equals(a.getAppointmentDate())
&& e.getAppointmentTime().equals(a.getAppointmentTime())
&& e.getAppointmentDuration().equals(a.getAppointmentDuration())
&& e.getAppointmentDuration().equals(a.getAppointmentPlace());
}
throw new IllegalArgumentException("Unsupported type for comparison");
}
private <T, K> void TestIfInIterable(Iterable<T> iterable, K expected) {
List<T> l = IterableToList(iterable);
boolean exists = l.stream().anyMatch(e -> matchesIgnoringId(expected, e));
assertTrue(exists, "");
}
/*
* Tests if an appointement made by the user himself and the users associated with appointment,
* the appoitement date, time, etc are correct.
*/
@Test
void testCreateAppointmentRequest_Users() {
/*
LocalDate date = LocalDate.parse("02-05-2025");
LocalTime duration = LocalTime.parse("00:15:30");
LocalTime time = LocalTime.parse("10:20:00");
String appointmentPlace = "salleInpulse";
String appointmentSubject = "Titanic";
Appointment appointment =
new Appointment(0L, date, time, duration, appointmentPlace, appointmentSubject);
sharedApiService.createAppointmentRequest(
appointment, "functional_entrepreneur@example.com");
Iterable<Appointment> appointments =
sharedApiService.getAppointmentsByProjectId(
functional_project.getIdProject(), "functional_entrepreneur@example.com");
List<Appointment> appointment_list = IterableToList(appointments);
assertEquals(date, date);
assertEquals(time, time);
assertEquals(appointmentPlace, appointmentPlace);
assertEquals(appointmentSubject, appointmentSubject);
*/
}
/*
* Tests the edge cases:
* - an appointement made by a user but has no participants.
* - the inputed dates for appointments are not older than current date.
* - date or time format is wrong.
*/
@Test
void testCreateAppointmentRequest_EdgeCases() {
assertEquals(0, 0);
}
/*
* Tests if an admin and entrepreneur with no prior appointments
* have no appointments.
*/
@Test
void testGetAppointement_EmptyUser() {
assertEquals(0, 0);
}
/*
* Tests if an admin and entrepreneur indepedant of eachother with no prior appointments,
* each have exactly one appointment an appointment after .
*/
@Test
void testGetAppointement_NonEmptyUser() {
assertEquals(0, 0);
}
/*
* Tests if an admin and entrepreneur both bound by the same project
* have the same appointment.
*/
@Test
void testGetAppointement_UsersHaveSameAppointement() {
assertEquals(0, 0);
}
/*
* Tests if in empty project has no sectionCells
*/
@Test
void testGetSectionCells_EmptyProject() {
assertEquals(0, 0);
}
/*
* Tests if in a project with no prior sectionCells that is given exactly
* one sectionCell has:
* - exactly one section cell.
* - the cell given back has the correct information.
*/
@Test
void testGetSectionCells_NonEmptyProject() {
assertEquals(0, 0);
}
/*
* Tests the edge cases:
* - sectionId is in {1, ... , 8}.
* - modificationDate is not newer than the current date.
*/
@Test
void testGetSectionCells_EdgeCases() {
assertEquals(0, 0);
}
/*
* Tests if:
* - handls a non existing projectId correctly.
* - returns the correct admin associated with project by id
*/
@Test
void testGetAdminByProjectId() {
assertEquals(0, 0);
}
/*
* Tests if:
* - handls non existing projectId correctly.
* - returns the correct entrepreneurs associated with the project.
*/
@Test
void testGetEntrepreneursByProjectId() {
assertEquals(0, 0);
}
}

View File

@ -0,0 +1,47 @@
## API Endpoints notes
### EntrepreneurApi and SharedApi
#### Endpoint Name Changes
- `/entrepreneur/lcsection/modify/{sectionId}``/entrepreneur/sectionCell/modify/{sectionId}`
### Admin api
- `/admin/appointments/upcoming`: is shared not admin
- `/admin/projects/decision`: instanciates classes with `adminId` instead of taking the id from the token
- `/admin/project/add`:
- point 1: the doc has this `projects` everywhere this should be `/admin/projects/add` to avoid confusion I think
- point 2: this doesn't assiociate users with a project I need to add other endopint for that
- `/admin/appoitements/report/{appointmentId}`:
- typo: `appoitements``appointments`
- `/admin/projects/remove/{projectId}`, `/admin/project/add`, `/admin/projects/decision`, `/admin/projects/pending`:
- should need token to delete or add project
### Entrepreneur api
- `/entrepreneur/sectionCell/modify/{sectionId}`:
- the section-id because of the definition of `sectionCell` schema the `sectionId` is given twice possibly leading to inconsistency. Which is why the path var to be removed:
- → `/entrepreneur/sectionCell/modify`
### Shared api
- `/shared/project/sectionCell/{projectId}/{sectionId}/{date}`:
- point 1:
same point for `project``projects`
- point 2:
have yet to read `sharedApiService` to see how dates are handled and to see if we agree on values of `date` to make it so it gets the version relative to current date
- `/shared/entrepreneurs/{projectId}`:
- maybe change to `/shared/projects/entrepreneurs/{projectId}` to match other similair endpoints like `/shared/projects/admin/{projectId}`
- `/shared/appointment/request`:
- creates the apointement but don't know how it associates other users, potentially multiple classes in one request body, is that possible ?
## TODOs for me
### list 1:
- add back-end server links (backend and auth) for interacting with api through swagger
- get config for that set up in the project
### list 2:
- see what to do about logo img
- see format for date and add it in examples
- ask the form of return of the json of iterables, for now I have put array
- add endpoint for adding users to a project
- update endpoint descriptions
- add examples for values in schemas

View File

@ -0,0 +1,11 @@
#!/bin/bash
cd ./swagger-ui
if [ ! -d "./node_modules/" ]
then
npm install
npm install swagger-cli
fi
npm start

View File

@ -0,0 +1,278 @@
# Admin API Endpoints
paths:
/admin/projects:
get:
operationId: getAdminProjects
summary: Get projects associated with the admin
tags:
- Admin API
security:
- MyINPulse: [MyINPulse-admin]
description: Retrieves a list of projects managed by the requesting admin, including key details for overview.
responses:
"200":
description: OK - List of projects returned successfully.
content:
application/json:
schema:
type: array
items:
$ref: "./main.yaml#/components/schemas/project"
"400":
description: Bad Request - Invalid project data provided (e.g., missing required fields).
"401":
description: Unauthorized - Authentication required or invalid token.
post:
operationId: addProjectManually
summary: Manually add a new project
description: Creates a new project with the provided details.
tags:
- Admin API
security:
- MyINPulse: [MyINPulse-admin]
requestBody:
required: true
description: Project details to create. `idProject` and `creationDate` will be ignored if sent and set by the server.
content:
application/json:
schema:
$ref: "./main.yaml#/components/schemas/project"
responses:
"201": # Use 201 Created for successful creation
description: Created - Project added successfully. Returns the created project.
content:
application/json:
schema:
$ref: "./main.yaml#/components/schemas/project"
"400":
description: Bad Request - Invalid project data provided (e.g., missing required fields).
"401":
description: Unauthorized.
/admin/projects/pending:
get:
operationId: getPendingProjects
summary: Get projects awaiting validation
tags:
- Admin API
security:
- MyINPulse: [MyINPulse-admin]
description: Retrieves a list of projects submitted by entrepreneurs that are pending admin approval.
responses:
"200":
description: OK - List of pending projects returned.
content:
application/json:
schema:
type: array
items:
$ref: "./main.yaml#/components/schemas/project" # Assuming pending projects use the same schema
"401":
description: Unauthorized.
/admin/projects/pending/decision/{pendingProjectId}:
post:
operationId: decidePendingProject
summary: Approve or reject a pending project
tags:
- Admin API
description: |-
Allows an admin to make a decision on a project awaiting validation.
If approved (decision=true), the project status changes, and it's linked to the involved users.
If rejected (decision=false), the pending project data might be archived or deleted based on business logic.
security:
- MyINPulse: [MyINPulse-admin]
parameters:
- in: path
name: pendingProjectId # Corrected typo and name change
required: true
schema:
type: integer
description: The ID of the pending project to decide upon.
example: 7
requestBody:
required: true
description: Decision payload.
content:
application/json:
schema:
$ref: './main.yaml#/components/schemas/projectDecision'
responses:
"204": # Use 204 No Content for successful action with no body
description: No Content - Decision processed successfully.
"400":
description: Bad Request - Invalid input (e.g., missing decision).
"401":
description: Unauthorized.
/admin/pending-accounts: # Path updated
get:
operationId: getPendingAccounts
summary: Get accounts awaiting validation
description: Retrieves a list of entrepreneur user accounts that are pending admin validation.
tags:
- Admin API
security:
- MyINPulse: [MyINPulse-admin]
responses:
"200":
description: OK - List of pending accounts returned.
content:
application/json:
schema:
type: array
items:
$ref: "./main.yaml#/components/schemas/user-entrepreneur"
"401":
description: Unauthorized.
/admin/accounts/validate/{userId}:
post: # Changed to POST as it changes state
operationId: validateUserAccount
summary: Validate a pending user account
description: Marks the user account specified by userId as validated/active.
tags:
- Admin API
security:
- MyINPulse: [MyINPulse-admin]
parameters:
- in: path
name: userId
required: true
schema:
type: integer
description: The ID of the user account to validate.
example: 102
responses:
"204":
description: No Content - Account validated successfully.
"400":
description: Bad Request - Invalid user ID format.
"401":
description: Unauthorized.
/admin/appointments/report/{appointmentId}:
post:
operationId: createAppointmentReport
summary: Create a report for an appointment
description: Creates and links a new report (e.g., meeting minutes) to the specified appointment using the provided content.
tags:
- Admin API
security:
- MyINPulse: [MyINPulse-admin]
parameters:
- in: path
name: appointmentId
required: true
schema:
type: integer
description: ID of the appointment to add a report to.
example: 303
requestBody:
required: true
description: Report content. `idReport` will be ignored if sent.
content:
application/json:
schema:
$ref: "./main.yaml#/components/schemas/report"
responses:
"201":
description: Created - Report created and linked successfully. Returns the created report.
content:
application/json:
schema: { $ref: "./main.yaml#/components/schemas/report" }
"400":
description: Bad Request - Invalid input (e.g., missing content, invalid appointment ID format).
"401":
description: Unauthorized.
put: # Changed to PUT for update/replacement
operationId: updateAppointmentReport
summary: Update an existing appointment report
description: Updates the content of an existing report linked to the specified appointment. Replaces the entire report content.
tags:
- Admin API
security:
- MyINPulse: [MyINPulse-admin]
parameters:
- in: path
name: appointmentId
required: true
schema:
type: integer
description: ID of the appointment whose report needs updating.
example: 303
requestBody:
required: true
description: New report content. `idReport` in the body should match the existing report's ID or will be ignored.
content:
application/json:
schema:
$ref: "./main.yaml#/components/schemas/report"
responses:
"200":
description: OK - Report updated successfully. Returns the updated report.
content:
application/json:
schema: { $ref: "./main.yaml#/components/schemas/report" }
"400":
description: Bad Request - Invalid input (e.g., missing content).
"401":
description: Unauthorized.
/admin/projects/{projectId}:
delete:
operationId: removeProject
summary: Remove a project
description: Permanently removes the project specified by projectId and potentially related data (use with caution).
tags:
- Admin API
security:
- MyINPulse: [MyINPulse-admin]
parameters:
- in: path
name: projectId
required: true
schema:
type: integer
description: The ID of the project to remove.
example: 12
responses:
"204":
description: No Content - Project removed successfully.
"400":
description: Bad Request - Invalid project ID format.
"401":
description: Unauthorized.
/admin/make-admin/{userId}:
post:
operationId: grantAdminRights
summary: Grant admin rights to a user
tags:
- Admin API
security:
- MyINPulse: [MyINPulse-admin]
description: Elevates the specified user to also have administrator privileges. Assumes the user already exists.
parameters:
- in: path
name: userId
required: true
schema:
type: integer
description: The ID of the user to grant admin rights.
example: 103
responses:
"204": # Use 204 No Content
description: No Content - Admin rights granted successfully.
"400":
description: Bad Request - Invalid user ID format or user is already an admin.
"401":
description: Unauthorized.

View File

@ -0,0 +1,872 @@
openapi: 3.0.3
info:
title: MyInpulse Backend API
description: 'This serves as an OpenAPI documentation for the MyInpulse backend service, covering operations for Entrepreneurs, Admins, and shared functionalities.'
version: 0.2.1
tags:
- name: Entrepreneurs API
description: API endpoints primarily for Entrepreneur users.
- name: Admin API
description: API endpoints restricted to Admin users for management tasks.
- name: Shared API
description: API endpoints accessible by both Entrepreneurs and Admins.
- name: Unauth API
description: API endpoints related to user account management.
components:
schemas:
user:
type: object
properties:
idUser:
type: integer
description: Unique identifier for the user.
example: 101
userSurname:
type: string
description: User's surname (last name).
example: Doe
userName:
type: string
description: User's given name (first name).
example: John
primaryMail:
type: string
format: email
description: User's primary email address.
example: john.doe@example.com
secondaryMail:
type: string
format: email
description: User's secondary email address (optional).
example: j.doe@personal.com
phoneNumber:
type: string
description: User's phone number.
example: '+33612345678'
user-entrepreneur:
allOf:
- $ref: '#/components/schemas/user'
- type: object
properties:
school:
type: string
description: The school the entrepreneur attends/attended.
example: ENSEIRB-MATMECA
course:
type: string
description: The specific course or program of study.
example: Electronics
sneeStatus:
type: boolean
description: Indicates if the user has SNEE status (Statut National d'Étudiant-Entrepreneur).
example: true
example:
idUser: 101
userSurname: Doe
userName: John
primaryMail: john.doe@example.com
secondaryMail: j.doe@personal.com
phoneNumber: '+33612345678'
school: ENSEIRB-MATMECA
course: Electronics
sneeStatus: true
user-admin:
allOf:
- $ref: '#/components/schemas/user'
example:
idUser: 55
userSurname: Admin
userName: Super
primaryMail: admin@myinpulse.com
phoneNumber: '+33512345678'
sectionCell:
type: object
description: Represents a cell (like a sticky note) within a specific section of a project's Lean Canvas.
properties:
idSectionCell:
type: integer
description: Unique identifier for the section cell.
example: 508
sectionId:
type: integer
description: 'Identifier of the Lean Canvas section this cell belongs to (e.g., 1 for Problem, 2 for Solution).'
example: 1
contentSectionCell:
type: string
description: The text content of the section cell.
example: Users find it hard to track project progress.
modificationDate:
type: string
format: date
description: The date when this cell was last modified.
example: '2025-04-15'
project:
type: object
description: Represents a project being managed or developed.
properties:
idProject:
type: integer
description: Unique identifier for the project.
example: 12
projectName:
type: string
description: The name of the project.
example: MyInpulse Mobile App
creationDate:
type: string
format: date
description: The date when the project was created in the system.
example: '2024-11-20'
logo:
type: string
format: byte
description: Base64 encoded string representing the project logo image.
example: /*Base64 encoded string representing the project logo image*/
status:
type: string
enum:
- PENDING
- ACTIVE
- ENDED
- ABORTED
- REJECTED
description: 'Corresponds to a status enum internal to the backend, it''s value in in requests incoming to the server should be ignored as the client shouldn''t be specifying them.'
example: NaN
report:
type: object
description: Represents a report associated with an appointment.
properties:
idReport:
type: integer
description: Unique identifier for the report.
example: 987
reportContent:
type: string
description: The textual content of the report. Could be plain text or Markdown (specify if known).
example: Discussed roadmap milestones for Q3. Agreed on preliminary UI mockups.
appointment:
type: object
description: Represents a scheduled meeting or appointment.
properties:
idAppointment:
type: integer
description: Unique identifier for the appointment.
example: 303
appointmentDate:
type: string
format: date
description: The date of the appointment.
example: '2025-05-10'
appointmentTime:
type: string
format: time
description: The time of the appointment (local time).
example: '14:30:00'
appointmentDuration:
type: string
description: 'Duration of the appointment in ISO 8601 duration format (e.g., PT1H30M for 1 hour 30 minutes).'
example: PT1H
appointmentPlace:
type: string
description: Location or meeting link for the appointment.
example: 'Meeting Room 3 / https://meet.example.com/abc-def-ghi'
appointmentSubject:
type: string
description: The main topic or subject of the appointment.
example: Q3 Roadmap Planning
joinRequest:
type: object
description: Represents a request from an entrepreneur to join an existing project.
properties:
projectId:
type: integer
description: The ID of the project the entrepreneur wants to join.
example: 12
projectDecision:
type: object
description: Represents a decision from an admin to accept a pending project.
properties:
projectId:
type: integer
description: The ID of the project the entrepreneur wants to join.
example: 12
adminId:
type: integer
description: The ID of the project the admin who will supervise the project in case of admission.
example: 2
isAccepted:
type: boolean
description: The boolean value of the decision.
example: 'true'
securitySchemes:
MyINPulse:
type: oauth2
description: OAuth2 authentication using Keycloak.
flows:
implicit:
authorizationUrl: '{keycloakBaseUrl}/realms/{keycloakRealm}/protocol/openid-connect/auth'
scopes:
MyINPulse-admin: Grants administrator access.
MyINPulse-entrepreneur: Grants standard entrepreneur user access.
servers:
- url: '{serverProtocol}://{serverHost}:{serverPort}'
description: API Server Environment
variables:
serverProtocol:
enum:
- http
- https
default: http
serverHost:
default: localhost
serverPort:
enum:
- '8081'
default: '8081'
keycloakBaseUrl:
default: 'http://localhost:7080'
description: Base URL for the Keycloak server.
keycloakRealm:
default: MyInpulseRealm
description: Keycloak realm name.
paths:
/unauth/finalize:
post:
summary: Finalize account setup using authentication token
description: |-
Completes the user account creation/setup process in the MyInpulse system.
This endpoint requires the user to be authenticated via Keycloak (e.g., after initial login).
User details (name, email, etc.) are extracted from the authenticated user's token (e.g., Keycloak JWT).
No request body is needed. The account is marked as pending admin validation upon successful finalization.
tags:
- Unauth API
responses:
'201':
description: Created - Account finalized and pending admin validation. Returns the user profile.
'400':
description: Bad Request - Problem processing the token or user data derived from it.
'401':
description: Unauthorized - Valid authentication token required.
'/unauth/request-join/{projectId}':
post:
summary: Request to join an existing project
description: Submits a request for the authenticated user (keycloack authenticated) to join the project specified by projectId. Their role is then changed to entrepreneur in server and Keycloak. This requires approval from a project admin.
tags:
- Unauth API
parameters:
- in: path
name: projectId
required: true
schema:
type: integer
description: The ID of the project to request joining.
example: 15
responses:
'202':
description: Accepted - Join request submitted and pending approval.
'400':
description: Bad Request - Invalid project ID format or already member/request pending.
'401':
description: Unauthorized.
/admin/pending-accounts:
get:
operationId: getPendingAccounts
summary: Get accounts awaiting validation
description: Retrieves a list of entrepreneur user accounts that are pending admin validation.
tags:
- Admin API
security:
- MyINPulse:
- MyINPulse-admin
responses:
'200':
description: OK - List of pending accounts returned.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/user-entrepreneur'
'401':
description: Unauthorized.
'/admin/accounts/validate/{userId}':
post:
operationId: validateUserAccount
summary: Validate a pending user account
description: Marks the user account specified by userId as validated/active.
tags:
- Admin API
security:
- MyINPulse:
- MyINPulse-admin
parameters:
- in: path
name: userId
required: true
schema:
type: integer
description: The ID of the user account to validate.
example: 102
responses:
'204':
description: No Content - Account validated successfully.
'400':
description: Bad Request - Invalid user ID format.
'401':
description: Unauthorized.
/admin/projects:
get:
operationId: getAdminProjects
summary: Get projects associated with the admin
tags:
- Admin API
security:
- MyINPulse:
- MyINPulse-admin
description: 'Retrieves a list of projects managed by the requesting admin, including key details for overview.'
responses:
'200':
description: OK - List of projects returned successfully.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/project'
'400':
description: 'Bad Request - Invalid project data provided (e.g., missing required fields).'
'401':
description: Unauthorized - Authentication required or invalid token.
post:
operationId: addProjectManually
summary: Manually add a new project
description: Creates a new project with the provided details.
tags:
- Admin API
security:
- MyINPulse:
- MyINPulse-admin
requestBody:
required: true
description: Project details to create. `idProject` and `creationDate` will be ignored if sent and set by the server.
content:
application/json:
schema:
$ref: '#/components/schemas/project'
responses:
'201':
description: Created - Project added successfully. Returns the created project.
content:
application/json:
schema:
$ref: '#/components/schemas/project'
'400':
description: 'Bad Request - Invalid project data provided (e.g., missing required fields).'
'401':
description: Unauthorized.
/admin/projects/pending:
get:
operationId: getPendingProjects
summary: Get projects awaiting validation
tags:
- Admin API
security:
- MyINPulse:
- MyINPulse-admin
description: Retrieves a list of projects submitted by entrepreneurs that are pending admin approval.
responses:
'200':
description: OK - List of pending projects returned.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/project'
'401':
description: Unauthorized.
'/admin/projects/pending/decision/{pendingProjectId}':
post:
operationId: decidePendingProject
summary: Approve or reject a pending project
tags:
- Admin API
description: |-
Allows an admin to make a decision on a project awaiting validation.
If approved (decision=true), the project status changes, and it's linked to the involved users.
If rejected (decision=false), the pending project data might be archived or deleted based on business logic.
security:
- MyINPulse:
- MyINPulse-admin
parameters:
- in: path
name: pendingProjectId
required: true
schema:
type: integer
description: The ID of the pending project to decide upon.
example: 7
requestBody:
required: true
description: Decision payload.
content:
application/json:
schema:
$ref: '#/components/schemas/projectDecision'
responses:
'204':
description: No Content - Decision processed successfully.
'400':
description: 'Bad Request - Invalid input (e.g., missing decision).'
'401':
description: Unauthorized.
'/admin/appointments/report/{appointmentId}':
post:
operationId: createAppointmentReport
summary: Create a report for an appointment
description: 'Creates and links a new report (e.g., meeting minutes) to the specified appointment using the provided content.'
tags:
- Admin API
security:
- MyINPulse:
- MyINPulse-admin
parameters:
- in: path
name: appointmentId
required: true
schema:
type: integer
description: ID of the appointment to add a report to.
example: 303
requestBody:
required: true
description: Report content. `idReport` will be ignored if sent.
content:
application/json:
schema:
$ref: '#/components/schemas/report'
responses:
'201':
description: Created - Report created and linked successfully. Returns the created report.
content:
application/json:
schema:
$ref: '#/components/schemas/report'
'400':
description: 'Bad Request - Invalid input (e.g., missing content, invalid appointment ID format).'
'401':
description: Unauthorized.
put:
operationId: updateAppointmentReport
summary: Update an existing appointment report
description: Updates the content of an existing report linked to the specified appointment. Replaces the entire report content.
tags:
- Admin API
security:
- MyINPulse:
- MyINPulse-admin
parameters:
- in: path
name: appointmentId
required: true
schema:
type: integer
description: ID of the appointment whose report needs updating.
example: 303
requestBody:
required: true
description: New report content. `idReport` in the body should match the existing report's ID or will be ignored.
content:
application/json:
schema:
$ref: '#/components/schemas/report'
responses:
'200':
description: OK - Report updated successfully. Returns the updated report.
content:
application/json:
schema:
$ref: '#/components/schemas/report'
'400':
description: 'Bad Request - Invalid input (e.g., missing content).'
'401':
description: Unauthorized.
'/admin/projects/{projectId}':
delete:
operationId: removeProject
summary: Remove a project
description: Permanently removes the project specified by projectId and potentially related data (use with caution).
tags:
- Admin API
security:
- MyINPulse:
- MyINPulse-admin
parameters:
- in: path
name: projectId
required: true
schema:
type: integer
description: The ID of the project to remove.
example: 12
responses:
'204':
description: No Content - Project removed successfully.
'400':
description: Bad Request - Invalid project ID format.
'401':
description: Unauthorized.
'/admin/make-admin/{userId}':
post:
operationId: grantAdminRights
summary: Grant admin rights to a user
tags:
- Admin API
security:
- MyINPulse:
- MyINPulse-admin
description: Elevates the specified user to also have administrator privileges. Assumes the user already exists.
parameters:
- in: path
name: userId
required: true
schema:
type: integer
description: The ID of the user to grant admin rights.
example: 103
responses:
'204':
description: No Content - Admin rights granted successfully.
'400':
description: Bad Request - Invalid user ID format or user is already an admin.
'401':
description: Unauthorized.
/shared/appointments/upcoming:
get:
operationId: getUpcomingAppointments
summary: Get upcoming appointments for the user
tags:
- Shared API
security:
- MyINPulse:
- MyINPulse-entrepreneur
- MyINPulse-admin
description: Retrieves a list of appointments scheduled for the authenticated user (either entrepreneur or admin) in the future.
responses:
'200':
description: OK - List of upcoming appointments.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/appointment'
'401':
description: Unauthorized.
'/shared/projects/sectionCells/{projectId}/{sectionId}/{date}':
get:
operationId: getSectionCellsByDate
summary: Get project section cells modified on a specific date
tags:
- Shared API
security:
- MyINPulse:
- MyINPulse-entrepreneur
- MyINPulse-admin
description: 'Retrieves section cells belonging to a specific section of a project, filtered by the last modification date. Requires user to have access to the project.'
parameters:
- in: path
name: projectId
required: true
schema:
type: integer
description: ID of the project.
- in: path
name: sectionId
required: true
schema:
type: integer
description: ID of the Lean Canvas section.
- in: path
name: date
required: true
schema:
type: string
format: date
description: The modification date to filter by (YYYY-MM-DD).
responses:
'200':
description: OK - List of section cells matching the criteria.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/sectionCell'
'400':
description: Bad Request - Invalid parameter format.
'401':
description: Unauthorized.
'/shared/projects/entrepreneurs/{projectId}':
get:
operationId: getProjectEntrepreneurs
summary: Get entrepreneurs associated with a project
tags:
- Shared API
security:
- MyINPulse:
- MyINPulse-entrepreneur
- MyINPulse-admin
description: Retrieves a list of entrepreneur users associated with the specified project. Requires access to the project.
parameters:
- in: path
name: projectId
required: true
schema:
type: integer
description: ID of the project.
responses:
'200':
description: OK - List of entrepreneurs.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/user-entrepreneur'
'401':
description: Unauthorized.
'403':
description: Forbidden - User does not have access to this project.
'404':
description: Not Found - Project not found.
'/shared/projects/admin/{projectId}':
get:
operationId: getProjectAdmins
summary: Get admins associated with a project
tags:
- Shared API
security:
- MyINPulse:
- MyINPulse-entrepreneur
- MyINPulse-admin
description: Retrieves a list of admin users associated with the specified project. Requires access to the project.
parameters:
- in: path
name: projectId
required: true
schema:
type: integer
description: ID of the project.
responses:
'200':
description: OK - List of admins.
content:
application/json:
schema:
$ref: '#/components/schemas/user-admin'
'401':
description: Unauthorized.
'403':
description: Forbidden - User does not have access to this project.
'404':
description: Not Found - Project not found.
'/shared/projects/appointments/{projectId}':
get:
operationId: getProjectAppointments
summary: Get appointments related to a project
tags:
- Shared API
security:
- MyINPulse:
- MyINPulse-entrepreneur
- MyINPulse-admin
description: Retrieves a list of appointments associated with the specified project. Requires access to the project.
parameters:
- in: path
name: projectId
required: true
schema:
type: integer
description: ID of the project.
responses:
'200':
description: OK - List of appointments.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/appointment'
'401':
description: Unauthorized.
'/shared/appointments/report/{appointmentId}':
get:
operationId: getAppointmentReport
summary: Get the report for an appointment
tags:
- Shared API
security:
- MyINPulse:
- MyINPulse-entrepreneur
- MyINPulse-admin
description: Retrieves the report associated with a specific appointment. Requires user to have access to the appointment/project.
parameters:
- in: path
name: appointmentId
required: true
schema:
type: integer
description: ID of the appointment.
responses:
'200':
description: OK - Report content returned.
content:
application/json:
schema:
$ref: '#/components/schemas/report'
'401':
description: Unauthorized.
/shared/appointments/request:
post:
operationId: requestAppointment
summary: Request a new appointment
tags:
- Shared API
security:
- MyINPulse:
- MyINPulse-entrepreneur
- MyINPulse-admin
description: 'Allows a user (entrepreneur or admin) to request a new appointment, potentially with another user or regarding a project. Details in the body. The request might need confirmation or create a pending appointment.'
requestBody:
required: true
description: Details of the appointment request.
content:
application/json:
schema:
$ref: '#/components/schemas/appointment'
example:
value:
appointmentDate: '2025-06-01'
appointmentTime: '10:00:00'
appointmentDuration: PT1H
appointmentPlace: Online
appointmentSubject: Follow-up on prototype
responses:
'202':
description: Accepted - Appointment request submitted.
content:
application/json:
schema:
$ref: '#/components/schemas/appointment'
'400':
description: Bad Request - Invalid appointment details.
'401':
description: Unauthorized.
/entrepreneur/projects/request:
post:
operationId: requestProjectCreation
summary: Request creation and validation of a new project
tags:
- Entrepreneurs API
description: |-
Submits a request for a new project. The project details are provided in the request body.
The requesting entrepreneur (identified by the token) will be associated to it.
The project is created with a 'pending' status, awaiting admin approval.
security:
- MyINPulse:
- MyINPulse-entrepreneur
requestBody:
required: true
description: 'Project details for the request. `status`, `creationDate` are required by the model when being sent but is ignored by the server; primarily expects a valid `projectId`, `name`, `logo`.'
content:
application/json:
schema:
$ref: '#/components/schemas/project'
responses:
'202':
description: Accepted - Project creation request received and is pending validation.
'400':
description: 'Bad Request - Invalid input (e.g., missing name).'
'401':
description: Unauthorized.
/entrepreneur/sectionCells:
post:
operationId: addSectionCell
summary: Add a cell to a Lean Canvas section
description: Adds a new cell (like a sticky note) with the provided content to a specific section of the entrepreneur's project's Lean Canvas. Assumes project context is known based on user's token. `idSectionCell` and `modificationDate` are server-generated so they're values in the request are ignored by the server.
tags:
- Entrepreneurs API
security:
- MyINPulse:
- MyINPulse-entrepreneur
requestBody:
required: true
description: Section cell details. `idSectionCell` and `modificationDate` will be ignored if sent.
content:
application/json:
schema:
$ref: '#/components/schemas/sectionCell'
responses:
'201':
description: Created - Section cell added successfully. Returns the created cell.
'400':
description: 'Bad Request - Invalid input (e.g., missing content or sectionId).'
'401':
description: Unauthorized.
'/entrepreneur/sectionCells/{sectionCellId}':
put:
operationId: modifySectionCell
summary: Modify data in a Lean Canvas section cell
description: Updates the content of an existing Lean Canvas section cell specified by `sectionCellId`. The server updates the `modificationDate`.
tags:
- Entrepreneurs API
security:
- MyINPulse:
- MyINPulse-entrepreneur
parameters:
- in: path
name: sectionCellId
required: true
schema:
type: integer
description: The ID of the section cell to modify.
example: 508
requestBody:
required: true
description: Updated section cell details. `idSectionCell` should match the path parameter. `modificationDate` will be updated by the server.
content:
application/json:
schema:
$ref: '#/components/schemas/sectionCell'
responses:
'200':
description: OK - Section cell updated successfully. Returns the updated cell.
'400':
description: Bad Request - Invalid input or ID mismatch.
'401':
description: Unauthorized.
delete:
operationId: removeSectionCell
summary: Remove a Lean Canvas section cell
description: Deletes the Lean Canvas section cell specified by `sectionCellId`.
tags:
- Entrepreneurs API
security:
- MyINPulse:
- MyINPulse-entrepreneur
parameters:
- in: path
name: sectionCellId
required: true
schema:
type: integer
description: The ID of the section cell to remove.
example: 509
responses:
'204':
description: No Content - Section cell removed successfully.
'400':
description: Bad Request - Invalid ID format.
'401':
description: Unauthorized.

View File

@ -0,0 +1,110 @@
# Entrepreneur API Endpoints
paths:
/entrepreneur/projects/request:
post:
operationId: requestProjectCreation
summary: Request creation and validation of a new project
tags:
- Entrepreneurs API
description: |-
Submits a request for a new project. The project details are provided in the request body.
The requesting entrepreneur (identified by the token) will be associated to it.
The project is created with a 'pending' status, awaiting admin approval.
security:
- MyINPulse: [MyINPulse-entrepreneur]
requestBody:
required: true
description: Project details for the request. `status`, `creationDate` are required by the model when being sent but is ignored by the server;
primarily expects a valid `projectId`, `name`, `logo`.
content:
application/json:
schema:
$ref: "./main.yaml#/components/schemas/project"
responses:
"202":
description: Accepted - Project creation request received and is pending validation.
"400":
description: Bad Request - Invalid input (e.g., missing name).
"401":
description: Unauthorized.
/entrepreneur/sectionCells: # Base path
post:
operationId: addSectionCell
summary: Add a cell to a Lean Canvas section
description: Adds a new cell (like a sticky note) with the provided content to a specific section of the entrepreneur's project's Lean Canvas. Assumes project context is known based on user's token.
`idSectionCell` and `modificationDate` are server-generated so they're values in the request are ignored by the server.
tags:
- Entrepreneurs API
security:
- MyINPulse: [MyINPulse-entrepreneur]
requestBody:
required: true
description: Section cell details. `idSectionCell` and `modificationDate` will be ignored if sent.
content:
application/json:
schema:
$ref: "./main.yaml#/components/schemas/sectionCell"
responses:
"201":
description: Created - Section cell added successfully. Returns the created cell.
"400":
description: Bad Request - Invalid input (e.g., missing content or sectionId).
"401":
description: Unauthorized.
/entrepreneur/sectionCells/{sectionCellId}:
put:
operationId: modifySectionCell
summary: Modify data in a Lean Canvas section cell
description: Updates the content of an existing Lean Canvas section cell specified by `sectionCellId`. The server updates the `modificationDate`.
tags:
- Entrepreneurs API
security:
- MyINPulse: [MyINPulse-entrepreneur]
parameters:
- in: path
name: sectionCellId
required: true
schema:
type: integer
description: The ID of the section cell to modify.
example: 508
requestBody:
required: true
description: Updated section cell details. `idSectionCell` should match the path parameter. `modificationDate` will be updated by the server.
content:
application/json:
schema:
$ref: "./main.yaml#/components/schemas/sectionCell"
responses:
"200":
description: OK - Section cell updated successfully. Returns the updated cell.
"400":
description: Bad Request - Invalid input or ID mismatch.
"401":
description: Unauthorized.
delete:
operationId: removeSectionCell
summary: Remove a Lean Canvas section cell
description: Deletes the Lean Canvas section cell specified by `sectionCellId`.
tags:
- Entrepreneurs API
security:
- MyINPulse: [MyINPulse-entrepreneur]
parameters:
- in: path
name: sectionCellId
required: true
schema:
type: integer
description: The ID of the section cell to remove.
example: 509
responses:
"204":
description: No Content - Section cell removed successfully.
"400":
description: Bad Request - Invalid ID format.
"401":
description: Unauthorized.

View File

@ -0,0 +1,140 @@
openapi: 3.0.3
info:
title: MyInpulse Backend API
description: This serves as an OpenAPI documentation for the MyInpulse backend service, covering operations for Entrepreneurs, Admins, and shared functionalities.
version: 0.2.1
tags:
- name: Entrepreneurs API
description: API endpoints primarily for Entrepreneur users.
- name: Admin API
description: API endpoints restricted to Admin users for management tasks.
- name: Shared API
description: API endpoints accessible by both Entrepreneurs and Admins.
- name: Unauth API
description: API endpoints related to user account management.
components:
schemas:
user:
$ref: "models.yaml#/user"
user-entrepreneur:
$ref: "models.yaml#/user-entrepreneur"
user-admin:
$ref: "models.yaml#/user-admin"
sectionCell:
$ref: "models.yaml#/sectionCell"
project:
$ref: "models.yaml#/project"
report:
$ref: "models.yaml#/report"
appointment:
$ref: "models.yaml#/appointment"
joinRequest:
$ref: "models.yaml#/joinRequest"
projectDecision:
$ref: "models.yaml#/projectDecision"
securitySchemes:
MyINPulse:
type: oauth2
description: OAuth2 authentication using Keycloak.
flows:
implicit:
authorizationUrl: '{keycloakBaseUrl}/realms/{keycloakRealm}/protocol/openid-connect/auth'
scopes:
MyINPulse-admin: Grants administrator access.
MyINPulse-entrepreneur: Grants standard entrepreneur user access.
servers:
- url: '{serverProtocol}://{serverHost}:{serverPort}'
description: API Server Environment
variables:
serverProtocol:
enum: [http, https]
default: http
serverHost:
default: localhost
serverPort:
enum: ['8081']
default: '8081'
keycloakBaseUrl:
default: http://localhost:7080
description: Base URL for the Keycloak server.
keycloakRealm:
default: MyInpulseRealm
description: Keycloak realm name.
paths:
# _ _ _ _ _ _
# | | | |_ __ __ _ _ _| |_| |__ / \ _ __ (_)
# | | | | '_ \ / _` | | | | __| '_ \ / _ \ | '_ \| |
# | |_| | | | | (_| | |_| | |_| | | |/ ___ \| |_) | |
# \___/|_| |_|\__,_|\__,_|\__|_| |_/_/ \_\ .__/|_|
# |_|
/unauth/finalize:
$ref: "./unauthApi.yaml#/paths/~1unauth~1finalize"
/unauth/request-join/{projectId}:
$ref: "./unauthApi.yaml#/paths/~1unauth~1request-join~1{projectId}"
# _ ____ __ __ ___ _ _ _ ____ ___
# / \ | _ \| \/ |_ _| \ | | / \ | _ \_ _|
# / _ \ | | | | |\/| || || \| | / _ \ | |_) | |
# / ___ \| |_| | | | || || |\ | / ___ \| __/| |
# /_/ \_\____/|_| |_|___|_| \_| /_/ \_\_| |___|
#
/admin/pending-accounts:
$ref: "./adminApi.yaml#/paths/~1admin~1pending-accounts"
/admin/accounts/validate/{userId}:
$ref: "./adminApi.yaml#/paths/~1admin~1accounts~1validate~1{userId}"
/admin/projects:
$ref: "./adminApi.yaml#/paths/~1admin~1projects"
/admin/projects/pending:
$ref: "./adminApi.yaml#/paths/~1admin~1projects~1pending"
/admin/projects/pending/decision/{pendingProjectId}:
$ref: "./adminApi.yaml#/paths/~1admin~1projects~1pending~1decision~1{pendingProjectId}"
/admin/appointments/report/{appointmentId}:
$ref: "./adminApi.yaml#/paths/~1admin~1appointments~1report~1{appointmentId}"
/admin/projects/{projectId}:
$ref: "./adminApi.yaml#/paths/~1admin~1projects~1{projectId}"
/admin/make-admin/{userId}: # Renamed for clarity
$ref: "./adminApi.yaml#/paths/~1admin~1make-admin~1{userId}"
# ____ _ _ _ ____ ___
# / ___|| |__ __ _ _ __ ___ __| | / \ | _ \_ _|
# \___ \| '_ \ / _` | '__/ _ \/ _` | / _ \ | |_) | |
# ___) | | | | (_| | | | __/ (_| | / ___ \| __/| |
# |____/|_| |_|\__,_|_| \___|\__,_| /_/ \_\_| |___|
#
/shared/appointments/upcoming:
$ref: "./sharedApi.yaml#/paths/~1shared~1appointments~1upcoming"
/shared/projects/sectionCells/{projectId}/{sectionId}/{date}:
$ref: "./sharedApi.yaml#/paths/~1shared~1projects~1sectionCells~1{projectId}~1{sectionId}~1{date}"
/shared/projects/entrepreneurs/{projectId}:
$ref: "./sharedApi.yaml#/paths/~1shared~1projects~1entrepreneurs~1{projectId}"
/shared/projects/admin/{projectId}:
$ref: "./sharedApi.yaml#/paths/~1shared~1projects~1admin~1{projectId}"
/shared/projects/appointments/{projectId}:
$ref: "./sharedApi.yaml#/paths/~1shared~1projects~1appointments~1{projectId}"
/shared/appointments/report/{appointmentId}:
$ref: "./sharedApi.yaml#/paths/~1shared~1appointments~1report~1{appointmentId}"
/shared/appointments/request:
$ref: "./sharedApi.yaml#/paths/~1shared~1appointments~1request"
# _____ _ _ _____ ____ _____ ____ ____ _____ _ _ _____ _ _ ____
# | ____| \ | |_ _| _ \| ____| _ \| _ \| ____| \ | | ____| | | | _ \
# | _| | \| | | | | |_) | _| | |_) | |_) | _| | \| | _| | | | | |_) |
# | |___| |\ | | | | _ <| |___| __/| _ <| |___| |\ | |___| |_| | _ <
# |_____|_|_\_| |_| |_| \_\_____|_| |_| \_\_____|_| \_|_____|\___/|_| \_\
# / \ | _ \_ _|
# / _ \ | |_) | |
# / ___ \| __/| |
# /_/ \_\_| |___|
#
/entrepreneur/projects/request:
$ref: "./entrepreneurApi.yaml#/paths/~1entrepreneur~1projects~1request"
/entrepreneur/sectionCells:
$ref: "./entrepreneurApi.yaml#/paths/~1entrepreneur~1sectionCells"
/entrepreneur/sectionCells/{sectionCellId}:
$ref: "./entrepreneurApi.yaml#/paths/~1entrepreneur~1sectionCells~1{sectionCellId}"

View File

@ -0,0 +1,200 @@
# models.yaml
user:
type: object
properties:
idUser:
type: integer
description: Unique identifier for the user.
#readOnly: true # Typically generated by the server
example: 101
userSurname:
type: string
description: User's surname (last name).
example: "Doe"
userName:
type: string
description: User's given name (first name).
example: "John"
primaryMail:
type: string
format: email
description: User's primary email address.
example: "john.doe@example.com"
secondaryMail:
type: string
format: email
description: User's secondary email address (optional).
example: "j.doe@personal.com"
phoneNumber:
type: string
description: User's phone number.
example: "+33612345678" # Example using international format
user-entrepreneur:
allOf:
- $ref: "#/user"
- type: object
properties:
school:
type: string
description: The school the entrepreneur attends/attended.
example: "ENSEIRB-MATMECA"
course:
type: string
description: The specific course or program of study.
example: "Electronics"
sneeStatus:
type: boolean
description: Indicates if the user has SNEE status (Statut National d'Étudiant-Entrepreneur).
example: true
example: # Added full object example
idUser: 101
userSurname: "Doe"
userName: "John"
primaryMail: "john.doe@example.com"
secondaryMail: "j.doe@personal.com"
phoneNumber: "+33612345678"
school: "ENSEIRB-MATMECA"
course: "Electronics"
sneeStatus: true
user-admin:
allOf:
- $ref: "#/user"
# No additional properties needed for this example
example: # Added full object example
idUser: 55
userSurname: "Admin"
userName: "Super"
primaryMail: "admin@myinpulse.com"
phoneNumber: "+33512345678"
sectionCell:
type: object
description: Represents a cell (like a sticky note) within a specific section of a project's Lean Canvas.
properties:
idSectionCell:
type: integer
description: Unique identifier for the section cell.
#readOnly: true # Generated by server
example: 508
sectionId:
type: integer
description: Identifier of the Lean Canvas section this cell belongs to (e.g., 1 for Problem, 2 for Solution).
example: 1
contentSectionCell:
type: string
description: The text content of the section cell.
example: "Users find it hard to track project progress."
modificationDate:
type: string
format: date # Using Java LocalDate -> YYYY-MM-DD
description: The date when this cell was last modified.
#readOnly: true # Typically updated by the server on modification
example: "2025-04-15"
project:
type: object
description: Represents a project being managed or developed.
properties:
idProject:
type: integer
description: Unique identifier for the project.
#readOnly: true # Generated by server
example: 12
projectName:
type: string
description: The name of the project.
example: "MyInpulse Mobile App"
creationDate:
type: string
format: date # Using Java LocalDate -> YYYY-MM-DD
description: The date when the project was created in the system.
#readOnly: true # Set by server
example: "2024-11-20"
logo:
type: string
format: byte
description: Base64 encoded string representing the project logo image.
example: "/*Base64 encoded string representing the project logo image*/"
status:
type: string
enum: [PENDING, ACTIVE, ENDED, ABORTED, REJECTED]
description: Corresponds to a status enum internal to the backend, it's value in in requests
incoming to the server should be ignored as the client shouldn't be specifying them.
example: "NaN"
report:
type: object
description: Represents a report associated with an appointment.
properties:
idReport:
type: integer
description: Unique identifier for the report.
#readOnly: true # Generated by server
example: 987
reportContent:
type: string
description: The textual content of the report. Could be plain text or Markdown (specify if known).
example: "Discussed roadmap milestones for Q3. Agreed on preliminary UI mockups."
appointment: # Corrected typo
type: object
description: Represents a scheduled meeting or appointment.
properties:
idAppointment: # Assuming there's an ID
type: integer
description: Unique identifier for the appointment.
#readOnly: true
example: 303
appointmentDate:
type: string
format: date # Using Java LocalDate -> YYYY-MM-DD
description: The date of the appointment.
example: "2025-05-10"
appointmentTime:
type: string
format: time # Using Java LocalTime -> HH:mm:ss
description: The time of the appointment (local time).
example: "14:30:00"
appointmentDuration:
type: string
description: Duration of the appointment in ISO 8601 duration format (e.g., PT1H30M for 1 hour 30 minutes).
example: "PT1H" # Example for 1 hour
appointmentPlace:
type: string
description: Location or meeting link for the appointment.
example: "Meeting Room 3 / https://meet.example.com/abc-def-ghi"
appointmentSubject:
type: string
description: The main topic or subject of the appointment.
example: "Q3 Roadmap Planning"
# Consider adding project ID or user IDs if relevant association exists
joinRequest:
type: object
description: Represents a request from an entrepreneur to join an existing project.
properties:
projectId:
type: integer
description: The ID of the project the entrepreneur wants to join.
example: 12
# Consider adding userId if the requester isn't implicit from auth context
# Consider adding a message field
projectDecision:
type: object
description: Represents a decision from an admin to accept a pending project.
properties:
projectId:
type: integer
description: The ID of the project the entrepreneur wants to join.
example: 12
adminId:
type: integer
description: The ID of the project the admin who will supervise the project in case of admission.
example: 2
isAccepted:
type: boolean
description: The boolean value of the decision.
example: "true"

View File

@ -0,0 +1,216 @@
# Shared API Endpoints
paths:
/shared/appointments/upcoming:
get:
operationId: getUpcomingAppointments
summary: Get upcoming appointments for the user
tags:
- Shared API
security:
- MyINPulse: [MyINPulse-entrepreneur, MyINPulse-admin] # Accessible by both
description: Retrieves a list of appointments scheduled for the authenticated user (either entrepreneur or admin) in the future.
responses:
"200":
description: OK - List of upcoming appointments.
content:
application/json:
schema:
type: array
items:
$ref: "./main.yaml#/components/schemas/appointment"
"401":
description: Unauthorized.
/shared/projects/sectionCells/{projectId}/{sectionId}/{date}:
get:
operationId: getSectionCellsByDate
summary: Get project section cells modified on a specific date
tags:
- Shared API
security:
- MyINPulse: [MyINPulse-entrepreneur, MyINPulse-admin]
description: Retrieves section cells belonging to a specific section of a project, filtered by the last modification date. Requires user to have access to the project.
parameters:
- in: path
name: projectId
required: true
schema: { type: integer }
description: ID of the project.
- in: path
name: sectionId
required: true
schema: { type: integer }
description: ID of the Lean Canvas section.
- in: path
name: date
required: true
schema: { type: string, format: date } # Expect YYYY-MM-DD
description: The modification date to filter by (YYYY-MM-DD).
responses:
"200":
description: OK - List of section cells matching the criteria.
content:
application/json:
schema:
type: array
items:
$ref: "./main.yaml#/components/schemas/sectionCell"
"400":
description: Bad Request - Invalid parameter format.
"401":
description: Unauthorized.
/shared/projects/entrepreneurs/{projectId}:
get:
operationId: getProjectEntrepreneurs
summary: Get entrepreneurs associated with a project
tags:
- Shared API
security:
- MyINPulse: [MyINPulse-entrepreneur, MyINPulse-admin]
description: Retrieves a list of entrepreneur users associated with the specified project. Requires access to the project.
parameters:
- in: path
name: projectId
required: true
schema: { type: integer }
description: ID of the project.
responses:
"200":
description: OK - List of entrepreneurs.
content:
application/json:
schema:
type: array
items:
$ref: "./main.yaml#/components/schemas/user-entrepreneur"
"401":
description: Unauthorized.
"403":
description: Forbidden - User does not have access to this project.
"404":
description: Not Found - Project not found.
/shared/projects/admin/{projectId}: # Path updated
get:
operationId: getProjectAdmins
summary: Get admins associated with a project
tags:
- Shared API
security:
- MyINPulse: [MyINPulse-entrepreneur, MyINPulse-admin]
description: Retrieves a list of admin users associated with the specified project. Requires access to the project.
parameters:
- in: path
name: projectId
required: true
schema: { type: integer }
description: ID of the project.
responses:
"200":
description: OK - List of admins.
content:
application/json:
schema:
$ref: "./main.yaml#/components/schemas/user-admin"
"401":
description: Unauthorized.
"403":
description: Forbidden - User does not have access to this project.
"404":
description: Not Found - Project not found.
/shared/projects/appointments/{projectId}:
get:
operationId: getProjectAppointments
summary: Get appointments related to a project
tags:
- Shared API
security:
- MyINPulse: [MyINPulse-entrepreneur, MyINPulse-admin]
description: Retrieves a list of appointments associated with the specified project. Requires access to the project.
parameters:
- in: path
name: projectId
required: true
schema: { type: integer }
description: ID of the project.
responses:
"200":
description: OK - List of appointments.
content:
application/json:
schema:
type: array
items:
$ref: "./main.yaml#/components/schemas/appointment"
"401":
description: Unauthorized.
/shared/appointments/report/{appointmentId}: # Path updated
get:
operationId: getAppointmentReport # Shared endpoint implies read-only access might be possible
summary: Get the report for an appointment
tags:
- Shared API
security:
- MyINPulse: [MyINPulse-entrepreneur, MyINPulse-admin]
description: Retrieves the report associated with a specific appointment. Requires user to have access to the appointment/project.
parameters:
- in: path
name: appointmentId
required: true
schema: { type: integer }
description: ID of the appointment.
responses:
"200":
description: OK - Report content returned.
content:
application/json:
schema:
$ref: "./main.yaml#/components/schemas/report"
"401":
description: Unauthorized.
/shared/appointments/request:
post:
operationId: requestAppointment
summary: Request a new appointment
tags:
- Shared API
security:
- MyINPulse: [MyINPulse-entrepreneur, MyINPulse-admin]
description: Allows a user (entrepreneur or admin) to request a new appointment, potentially with another user or regarding a project. Details in the body. The request might need confirmation or create a pending appointment.
requestBody:
required: true
description: Details of the appointment request.
content:
application/json:
schema:
$ref: "./main.yaml#/components/schemas/appointment" # Assuming request uses same model structure
example:
value:
appointmentDate: "2025-06-01"
appointmentTime: "10:00:00"
appointmentDuration: "PT1H"
appointmentPlace: "Online"
appointmentSubject: "Follow-up on prototype"
# Potentially add projectId or targetUserId here
responses:
"202": # Accepted seems appropriate for a request
description: Accepted - Appointment request submitted.
content:
application/json: # Optionally return the pending appointment data
schema:
$ref: "./main.yaml#/components/schemas/appointment"
"400":
description: Bad Request - Invalid appointment details.
"401":
description: Unauthorized.

View File

@ -0,0 +1,60 @@
# _ _ _ _ _ _
# | | | |_ __ __ _ _ _| |_| |__ / \ _ __ (_)
# | | | | '_ \ / _` | | | | __| '_ \ / _ \ | '_ \| |
# | |_| | | | | (_| | |_| | |_| | | |/ ___ \| |_) | |
# \___/|_| |_|\__,_|\__,_|\__|_| |_/_/ \_\ .__/|_|
# |_|
paths:
/unauth/finalize:
post:
summary: Finalize account setup using authentication token
description: |-
Completes the user account creation/setup process in the MyInpulse system.
This endpoint requires the user to be authenticated via Keycloak (e.g., after initial login).
User details (name, email, etc.) are extracted from the authenticated user's token (e.g., Keycloak JWT).
No request body is needed. The account is marked as pending admin validation upon successful finalization.
tags:
- Unauth API
responses:
"201":
description: Created - Account finalized and pending admin validation. Returns the user profile.
"400":
description: Bad Request - Problem processing the token or user data derived from it.
"401":
description: Unauthorized - Valid authentication token required.
/unauth/request-join/{projectId}:
post:
summary: Request to join an existing project
description: Submits a request for the authenticated user (keycloack authenticated) to join the project specified by projectId. Their role is then changed to entrepreneur in server and Keycloak. This requires approval from a project admin.
tags:
- Unauth API
parameters:
- in: path
name: projectId
required: true
schema:
type: integer
description: The ID of the project to request joining.
example: 15
responses: # Moved responses block to correct level
"202":
description: Accepted - Join request submitted and pending approval.
"400":
description: Bad Request - Invalid project ID format or already member/request pending.
"401":
description: Unauthorized.
/unauth/request-admin-role:
post:
summary: Request to join an existing project
description: Submits a request for the authenticated user (keycloack authenticated) to become an admin. Their role is then changed to admin in server and Keycloak. This requires approval from a project admin.
tags:
- Unauth API
responses:
"202":
description: Accepted - Become admin request submitted and pending approval.
"400":
description: Bad Request - Invalid project ID format or already member/request pending.
"401":
description: Unauthorized.

View File

@ -0,0 +1,14 @@
const express = require("express");
const swaggerUi = require("swagger-ui-express");
const yaml = require("js-yaml");
const fs = require("fs");
const app = express();
const swaggerDocument = yaml.load(fs.readFileSync("../src/bundled.yaml", "utf8"));
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument));
app.listen(3000, () => {
console.log("Swagger UI running at http://localhost:3000/api-docs");
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
{
"name": "swagger-ui",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"bundle": "swagger-cli bundle -o ../src/bundled.yaml -t yaml ../src/main.yaml",
"start": "npm run bundle; node main.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"express": "^4.21.2",
"js-yaml": "^4.1.0",
"package.json": "^2.0.1",
"swagger-cli": "^4.0.4",
"swagger-ui-express": "^5.0.1"
}
}

View File

@ -4,6 +4,7 @@ import { callApi } from "@/services/api.ts";
import { ref } from "vue";
const CustomRequest = ref("");
const USERID = ref("");
</script>
<template>
@ -34,30 +35,7 @@ const CustomRequest = ref("");
<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" />
@ -66,6 +44,83 @@ const CustomRequest = ref("");
<button @click="callApi(CustomRequest)">call</button>
</td>
</tr>
<tr>
<td>Create an account</td>
<td>
<button @click="callApi('unauth/create_account')">
call
</button>
</td>
<td>res</td>
<td id="4"></td>
</tr>
<tr>
<td>Get Pending Accounts</td>
<td>
<button @click="callApi('admin/get_pending_accounts')">
call
</button>
</td>
<td>res</td>
<td id="6"></td>
</tr>
<tr>
<td>admin/validate_user_account/{id}</td>
<td>
<button
@click="
callApi('admin/validate_user_account/' + USERID)
"
>
call
</button>
</td>
<td>
<input v-model="USERID" placeholder="user ID" />
</td>
<td id="5"></td>
</tr>
<tr>
<td>admin/setadmin/{uid}</td>
<td>
<button @click="callApi('admin/setadmin/' + USERID)">
call
</button>
</td>
<td>
<input v-model="USERID" placeholder="user ID" />
</td>
</tr>
<tr>
<td>Unauth API call</td>
<td>
<button @click="callApi('unauth/dev')">call</button>
</td>
<td>res</td>
<td id="8"></td>
</tr>
<tr>
<td>Unauth API call</td>
<td>
<button @click="callApi('unauth/dev')">call</button>
</td>
<td>res</td>
<td id="9"></td>
</tr>
<tr>
<td>Unauth API call</td>
<td>
<button @click="callApi('unauth/dev')">call</button>
</td>
<td>res</td>
<td id="10"></td>
</tr>
</tbody>
</table>
</template>