diff --git a/back_end/src/main/java/hackathon/FrisbYEE/jpa/dto/AthleteDTO.java b/back_end/src/main/java/hackathon/FrisbYEE/jpa/dto/AthleteDTO.java index 572e2df..41e4a97 100644 --- a/back_end/src/main/java/hackathon/FrisbYEE/jpa/dto/AthleteDTO.java +++ b/back_end/src/main/java/hackathon/FrisbYEE/jpa/dto/AthleteDTO.java @@ -7,6 +7,7 @@ import java.util.List; @Data public class AthleteDTO { + private Integer id; private String id_keycloak; private String name; private String prenom; diff --git a/back_end/src/main/java/hackathon/FrisbYEE/jpa/dto/CoachDTO.java b/back_end/src/main/java/hackathon/FrisbYEE/jpa/dto/CoachDTO.java index 7c33230..760b12b 100644 --- a/back_end/src/main/java/hackathon/FrisbYEE/jpa/dto/CoachDTO.java +++ b/back_end/src/main/java/hackathon/FrisbYEE/jpa/dto/CoachDTO.java @@ -6,6 +6,7 @@ import lombok.Data; @Data public class CoachDTO { + private Integer id; private String id_keycloak; private String name; private String prenom; diff --git a/back_end/src/main/java/hackathon/FrisbYEE/jpa/dto/UserDTO.java b/back_end/src/main/java/hackathon/FrisbYEE/jpa/dto/UserDTO.java index d7a9278..40c13d7 100644 --- a/back_end/src/main/java/hackathon/FrisbYEE/jpa/dto/UserDTO.java +++ b/back_end/src/main/java/hackathon/FrisbYEE/jpa/dto/UserDTO.java @@ -5,6 +5,7 @@ import lombok.Data; @Data public class UserDTO { + private Integer id; private String id_keycloak; private String name; private String prenom; diff --git a/back_end/src/main/java/hackathon/FrisbYEE/jpa/service/UserDAO.java b/back_end/src/main/java/hackathon/FrisbYEE/jpa/service/UserDAO.java index 5481dbe..d63ee5c 100644 --- a/back_end/src/main/java/hackathon/FrisbYEE/jpa/service/UserDAO.java +++ b/back_end/src/main/java/hackathon/FrisbYEE/jpa/service/UserDAO.java @@ -1,10 +1,14 @@ package hackathon.FrisbYEE.jpa.service; +import hackathon.FrisbYEE.jpa.metier.Coach; import hackathon.FrisbYEE.jpa.metier.User; + +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface UserDAO extends JpaRepository { - + Optional findByKeycloakId(String keycloakId); } diff --git a/back_end/src/main/java/hackathon/FrisbYEE/rest/AthleteResource.java b/back_end/src/main/java/hackathon/FrisbYEE/rest/AthleteResource.java index 4e1a24c..30fc39b 100644 --- a/back_end/src/main/java/hackathon/FrisbYEE/rest/AthleteResource.java +++ b/back_end/src/main/java/hackathon/FrisbYEE/rest/AthleteResource.java @@ -128,6 +128,7 @@ public class AthleteResource { private AthleteDTO mapToDTO(Athlete athlete) { AthleteDTO dto = new AthleteDTO(); + dto.setId(athlete.getId()); dto.setId_keycloak(athlete.getKeycloakId()); dto.setName(athlete.getName()); dto.setPrenom(athlete.getPrenom()); @@ -138,6 +139,7 @@ public class AthleteResource { private Athlete mapToEntity(AthleteDTO dto) { Athlete athlete = new Athlete(); + athlete.setId(dto.getId()); athlete.setName(dto.getName()); athlete.setPrenom(dto.getPrenom()); athlete.setKeycloakId(dto.getId_keycloak()); diff --git a/back_end/src/main/java/hackathon/FrisbYEE/rest/CoachResource.java b/back_end/src/main/java/hackathon/FrisbYEE/rest/CoachResource.java index 1b054eb..19415d2 100644 --- a/back_end/src/main/java/hackathon/FrisbYEE/rest/CoachResource.java +++ b/back_end/src/main/java/hackathon/FrisbYEE/rest/CoachResource.java @@ -70,6 +70,7 @@ public class CoachResource { private CoachDTO mapToDTO(Coach coach) { CoachDTO dto = new CoachDTO(); + dto.setId(coach.getId()); dto.setId_keycloak(coach.getKeycloakId()); dto.setName(coach.getName()); return dto; @@ -77,6 +78,7 @@ public class CoachResource { private Coach mapToEntity(CoachDTO dto) { Coach coach = new Coach(); + coach.setId(dto.getId()); coach.setKeycloakId(dto.getId_keycloak()); coach.setName(dto.getName()); return coach; diff --git a/back_end/src/main/java/hackathon/FrisbYEE/rest/UserResource.java b/back_end/src/main/java/hackathon/FrisbYEE/rest/UserResource.java new file mode 100644 index 0000000..2a53742 --- /dev/null +++ b/back_end/src/main/java/hackathon/FrisbYEE/rest/UserResource.java @@ -0,0 +1,72 @@ +package hackathon.FrisbYEE.rest; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import hackathon.FrisbYEE.jpa.dto.AthleteDTO; +import hackathon.FrisbYEE.jpa.dto.UserDTO; +import hackathon.FrisbYEE.jpa.metier.Athlete; +import hackathon.FrisbYEE.jpa.metier.User; +import hackathon.FrisbYEE.jpa.service.UserDAO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; + +@RestController +@RequestMapping("/users") +@CrossOrigin(origins = "http://localhost:3000") +public class UserResource { + @Autowired + private UserDAO userDAO; + + @Operation(summary = "Récupère tous les utilisateurs") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Récupère tous les athlètes", content = @Content(mediaType = "application/json", schema = @Schema(implementation = List.class))) + }) + @GetMapping("/all") + @PreAuthorize("hasRole('admin') or hasRole('coach')") + public ResponseEntity> all() { + List users = userDAO.findAll(); + List dtos = new ArrayList<>(); + for (User user : users) { + dtos.add(mapToDTO(user)); + } + return ResponseEntity.ok(dtos); + } + + @Operation(summary = "Récupère l'utilisateur ayant l'identifiant correspondant") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Récupération effectuée", content = @Content(mediaType = "application/json", schema = @Schema(implementation = UserDTO.class))) + }) + @GetMapping("/{id}") + @PreAuthorize("hasRole('admin') or hasRole('coach') or hasRole('athlete')") + public ResponseEntity getById(@PathVariable String id) { + User user = userDAO.findByKeycloakId(id).get(); + return ResponseEntity.ok(mapToDTO(user)); + } + + private UserDTO mapToDTO(User user) { + UserDTO dto = new UserDTO(); + dto.setId(user.getId()); + dto.setId_keycloak(user.getKeycloakId()); + dto.setName(user.getName()); + dto.setPrenom(user.getPrenom()); + dto.setRole(user.getRole()); + + return dto; + } +} \ No newline at end of file diff --git a/back_end/src/main/java/hackathon/FrisbYEE/rest/UserSyncResource.java b/back_end/src/main/java/hackathon/FrisbYEE/rest/UserSyncResource.java index 1f5f302..f4adae2 100644 --- a/back_end/src/main/java/hackathon/FrisbYEE/rest/UserSyncResource.java +++ b/back_end/src/main/java/hackathon/FrisbYEE/rest/UserSyncResource.java @@ -9,18 +9,15 @@ import org.springframework.security.core.context.SecurityContextHolder; import hackathon.FrisbYEE.jpa.metier.Athlete; import hackathon.FrisbYEE.jpa.service.AthleteDAO; import jakarta.transaction.Transactional; - import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; - import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/users") @CrossOrigin(origins = "http://localhost:3000") public class UserSyncResource { - @Autowired private AthleteDAO athleteDAO; @@ -28,13 +25,10 @@ public class UserSyncResource { @Transactional public ResponseEntity sync() { Jwt jwt = (Jwt) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - String keycloakId = jwt.getSubject(); String firstName = jwt.getClaimAsString("given_name"); String lastName = jwt.getClaimAsString("family_name"); - if (!athleteDAO.existsByKeycloakId(keycloakId)) { - System.out.println("New user detected from Keycloak. Syncing: " + firstName + " " + lastName); Athlete athlete = new Athlete(); athlete.setKeycloakId(keycloakId); athlete.setName(lastName); @@ -42,7 +36,6 @@ public class UserSyncResource { athlete.setRole(hackathon.FrisbYEE.jpa.metier.Role.athlete); athleteDAO.save(athlete); } - return ResponseEntity.ok().build(); } } \ No newline at end of file diff --git a/front_end/src/App.tsx b/front_end/src/App.tsx index 733add2..5e78330 100644 --- a/front_end/src/App.tsx +++ b/front_end/src/App.tsx @@ -11,21 +11,13 @@ import EdtCoach from './components/edt_coach' import { Coach } from "./classes"; import RessourcePanel from './components/ressourcePanel'; import TestAPI from './components/test_api'; -import EdtAthlete from './components/edt_athlete'; -// Test -const testCoach = new Coach(); -testCoach.id = 1; -testCoach.nom = "Coach Test"; const keycloakInitOptions = { onLoad: 'login-required', checkLoginIframe: false } - function App() { - - return ( @@ -36,7 +28,6 @@ function App() { - diff --git a/front_end/src/api.ts b/front_end/src/api.ts index 9c8ae21..6e7baa8 100644 --- a/front_end/src/api.ts +++ b/front_end/src/api.ts @@ -1,14 +1,18 @@ import axios from "axios"; + import keycloak from "./keycloak"; + const api = axios.create({ - baseURL: "http://localhost:8081/api", + // backend listens on 8081 and controllers are mounted at root (no /api prefix) + baseURL: "http://localhost:8081", headers: { "Content-Type": "application/json", }, withCredentials: true, }); +// Simple interceptor to ensure headers object exists; actual token should be set via setAuthToken() api.interceptors.request.use((config) => { if (keycloak?.token) { // eslint-disable-next-line no-param-reassign @@ -18,10 +22,23 @@ api.interceptors.request.use((config) => { return config; }); +// Helpers to set/clear the Authorization header programmatically (call after Keycloak login) +export function setAuthToken(token: string | null | undefined) { + if (token) { + api.defaults.headers.common["Authorization"] = `Bearer ${token}`; + } else { + delete api.defaults.headers.common["Authorization"]; + } +} + +export function clearAuthToken() { + delete api.defaults.headers.common["Authorization"]; +} + export const athleteService = { create: (data: any) => api.post("/athletes/create", data), getAll: () => api.get("/athletes/all"), - getById: (id: number | string) => api.get(`/athletes/${id}`), + getByKeycloakId: (id: number | string) => api.get(`/athletes/${id}`), update: (id: number | string, data: any) => api.put(`/athletes/${id}`, data), delete: (id: number | string) => api.delete(`/athletes/${id}`), @@ -63,7 +80,7 @@ export const coachService = { // controller doesn't declare a class-level path consistently; support both common patterns create: (data: any) => api.post(`/coach/create`, data), getAll: () => api.get(`/coach/all`), - getById: (id: number | string) => api.get(`/coach/${id}`), + getByKeycloakId: (id: number | string) => api.get(`/coach/${id}`), update: (id: number | string, data: any) => api.put(`/coach/update/${id}`, data), delete: (id: number | string) => api.delete(`/coach/delete/${id}`), @@ -73,7 +90,7 @@ export const coachService = { }; export const userService = { - getById: (id: number | string) => api.get(`/users/${id}`), + getByKeycloakId: (id: number | string) => api.get(`/users/${id}`), getAll: () => api.get(`/users`), }; diff --git a/front_end/src/components/StatsAthlete.tsx b/front_end/src/components/StatsAthlete.tsx new file mode 100644 index 0000000..e2f136f --- /dev/null +++ b/front_end/src/components/StatsAthlete.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import { Athlete, Session } from "../classes"; +import { calculStatsAthlete, niveauAlerte, StatsAthlete } from "../utils/athleteUtils" + +interface AthleteStatsProps { + athlete: Athlete; + sessions: Session[]; +} + +function StatAthlete({ athlete, sessions }: AthleteStatsProps) { + const [dateDebut, setDateDebut] = React.useState(new Date()); + const [dateFin, setDateFin] = React.useState(new Date()); + const [seuilCritique, setSeuilCritique] = React.useState(0); + const [seuilMax, setSeuilMax] = React.useState(0); + const [stats, setStats] = React.useState(null); + + const dateToDatetimeLocal = (date: Date) => { + const pad = (n: number) => n.toString().padStart(2, "0"); + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; + }; + + const handleCalculerStats = () => { + const statsCalculees = calculStatsAthlete(sessions, athlete, dateDebut, dateFin); + setStats(statsCalculees); + }; + + return ( +
+ + + + + + + + + + + {stats && ( +
+

Statistiques de {athlete.nom}

+

Nombre total de sessions : {stats.nbSessions}

+

Sessions par semaine : {stats.nbSessionsPerWeek.toFixed(2)}

+

Statut : {niveauAlerte(stats, seuilCritique, seuilMax)}

+ + {stats.distributions.size > 0 && ( + <> +

Distribution des activités :

+
    + {Array.from(stats.distributions.entries()).map(([nomActivite, count]) => ( +
  • + {nomActivite} : {count} session(s) +
  • + ))} +
+ + )} +
+ )} +
+ ); +} + +export default StatAthlete; \ No newline at end of file diff --git a/front_end/src/components/edt_athlete.tsx b/front_end/src/components/edt_athlete.tsx deleted file mode 100644 index c246705..0000000 --- a/front_end/src/components/edt_athlete.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { useState } from 'react'; - -export const EdtAthlete = () => { - const [formData, setFormData] = useState({ - name: '', - prenom: '', - id_keycloak: '', - categorie: '', - niveau: '' - }); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - try { - const response = await fetch("http://localhost:8081/api/athlete/create", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(formData), - }); - - if (response.ok) { - alert("Athlete created successfully in PostgreSQL!"); - setFormData({ name: '', prenom: '', id_keycloak: '', categorie: '', niveau: '' }); - } else { - alert("Failed to create athlete. Status: " + response.status); - } - } catch (error) { - console.error("Error creating athlete:", error); - alert("Error: Check console"); - } - }; - - return ( -
-

Test Create Athlete (PostgreSQL)

-
-
- - setFormData({...formData, name: e.target.value})} /> -
-
- - setFormData({...formData, prenom: e.target.value})} /> -
-
- - setFormData({...formData, id_keycloak: e.target.value})} /> -
-
- - setFormData({...formData, categorie: e.target.value})} /> -
-
- - setFormData({...formData, niveau: e.target.value})} /> -
- -
-
- ); -}; - -export default EdtAthlete; diff --git a/front_end/src/components/login.tsx b/front_end/src/components/login.tsx index fd4685d..e01f7f0 100644 --- a/front_end/src/components/login.tsx +++ b/front_end/src/components/login.tsx @@ -3,6 +3,7 @@ import { useEffect } from 'react'; import { Athlete, User } from '../classes'; import { useLocalData } from '../context/useLocalData'; import { postAthlete } from '../requetes'; +import { clearAuthToken, setAuthToken } from '../api'; export const Login =() =>{ const {user,setUser} = useLocalData() @@ -13,7 +14,8 @@ export const Login =() =>{ const newAthlete:Athlete = new Athlete() const athlete:Athlete = await postAthlete(newAthlete) - + + setAuthToken(keycloak.token); setUser(athlete); /*postAthlete if (keycloak.authenticated && keycloak.token) { @@ -56,6 +58,7 @@ export const Login =() =>{ function handleLogout(): void { keycloak.logout() setUser(new User()); + clearAuthToken(); } return(
diff --git a/front_end/src/components/object/user.tsx b/front_end/src/components/object/user.tsx index 9846ee3..38dbb0d 100644 --- a/front_end/src/components/object/user.tsx +++ b/front_end/src/components/object/user.tsx @@ -8,7 +8,7 @@ import {delay} from "../../requetes"; import CreateActivite from '../createActivite'; import { useLocalData } from '../../context/useLocalData'; import ObjectSession from './session'; - +import StatAthlete from '../StatsAthlete'; type Props = { admin?:Admin|null; @@ -115,10 +115,15 @@ function ObjectUser({admin=null,athlete=null,coach=null}:Props){ {/* TODO */}
+ {athlete !== null && ( +
+ +
+ )} - } - + } + ) }