FEAT : Mise en place de l'architecture + instription d'un utilisateur

This commit is contained in:
2025-12-01 12:14:57 +01:00
parent c76592aa65
commit 6f9bbe47ab
41 changed files with 672 additions and 3 deletions

2
.gitignore vendored
View File

@@ -31,4 +31,4 @@ build/
### VS Code ###
.vscode/
.env
src/main/resources/application.properties

View File

@@ -83,6 +83,15 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
</dependencies>
<build>

View File

@@ -0,0 +1,57 @@
package local.epul4a.fotosharing.controller;
import local.epul4a.fotosharing.model.Utilisateur;
import local.epul4a.fotosharing.repository.UtilisateurRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
@Controller
public class AuthController {
private final UtilisateurRepository utilisateurRepository;
private final PasswordEncoder passwordEncoder;
public AuthController(UtilisateurRepository utilisateurRepository, PasswordEncoder passwordEncoder) {
this.utilisateurRepository = utilisateurRepository;
this.passwordEncoder = passwordEncoder;
}
@GetMapping("/login")
public String loginPage(@RequestParam(value = "error", required = false) String error,
@RequestParam(value = "logout", required = false) String logout,
Model model) {
model.addAttribute("error", error != null);
model.addAttribute("logout", logout != null);
return "login";
}
@GetMapping("/register")
public String registerForm(Model model) {
model.addAttribute("utilisateur", new Utilisateur());
return "register";
}
@PostMapping("/register")
public String doRegister(@ModelAttribute("utilisateur") @Valid Utilisateur utilisateur,
BindingResult bindingResult, Model model) {
if (bindingResult.hasErrors()) {
return "register";
}
if (utilisateurRepository.findByEmail(utilisateur.getEmail()).isPresent()) {
model.addAttribute("error", "Email déjà utilisé");
return "register";
}
// encoder le mot de passe puis sauvegarder
utilisateur.setMotDePasse(passwordEncoder.encode(utilisateur.getMotDePasse()));
utilisateur.setActif(true);
utilisateurRepository.save(utilisateur);
return "redirect:/login?registered";
}
}

View File

@@ -0,0 +1,72 @@
package local.epul4a.fotosharing.controller;
import local.epul4a.fotosharing.model.Photo;
import local.epul4a.fotosharing.security.CustomUserDetails;
import local.epul4a.fotosharing.service.PhotoService;
import org.springframework.core.io.Resource;
import org.springframework.core.io.PathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.nio.file.Path;
@Controller
public class PhotoController {
private final PhotoService photoService;
public PhotoController(PhotoService photoService) {
this.photoService = photoService;
}
@GetMapping("/")
public String home() {
return "home"; // créer une page home.html simple
}
@GetMapping("/upload")
public String uploadForm() {
return "upload";
}
@PostMapping("/upload")
public String doUpload(@RequestParam("file") MultipartFile file,
@RequestParam(value="visibilite", defaultValue = "PRIVATE") String visibilite,
@AuthenticationPrincipal CustomUserDetails user,
Model model) {
try {
Photo p = photoService.store(file, visibilite, user.getUsername());
model.addAttribute("message", "Upload OK : " + p.getId());
return "redirect:/"; // ou page d'affichage
} catch (Exception e) {
model.addAttribute("error", e.getMessage());
return "upload";
}
}
@GetMapping("/photo/{id}/raw")
public ResponseEntity<Resource> rawPhoto(@PathVariable("id") String idOrUuid) {
// idOrUuid peut être uuid stocké ou id numeric ; ici on assume uuid
Path p = photoService.loadAsPath(idOrUuid);
Resource r = new PathResource(p);
if (!r.exists()) {
return ResponseEntity.notFound().build();
}
String contentType = "application/octet-stream";
try {
// tentative de détection basique
contentType = java.nio.file.Files.probeContentType(p);
} catch (Exception ignored) {}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType != null ? contentType : "application/octet-stream"))
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + p.getFileName().toString() + "\"")
.body(r);
}
}

View File

@@ -0,0 +1,31 @@
package local.epul4a.fotosharing.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@Entity
@Getter
@Setter
public class Commentaire {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String contenu;
private LocalDateTime dateCommentaire;
@ManyToOne
@JoinColumn(name = "id_photo")
private Photo photo;
@ManyToOne
@JoinColumn(name = "id_utilisateur")
private Utilisateur auteur;
// Getters & Setters
}

View File

@@ -0,0 +1,25 @@
package local.epul4a.fotosharing.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class Partage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "id_photo")
private Photo photo;
@ManyToOne
@JoinColumn(name = "id_utilisateur")
private Utilisateur utilisateur;
// Getters & Setters
}

View File

@@ -0,0 +1,32 @@
package local.epul4a.fotosharing.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Getter
@Setter
public class Photo {
public enum Visibilite { PRIVATE, PUBLIC, SHARED }
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nomFichierOriginal;
private String uuidFichier;
private LocalDateTime dateUpload;
@Enumerated(EnumType.STRING)
private Visibilite visibilite;
@ManyToOne
@JoinColumn(name = "id_utilisateur")
private Utilisateur proprietaire;
// getters & setters
}

View File

@@ -0,0 +1,34 @@
package local.epul4a.fotosharing.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Entity
@Getter
@Setter
public class Utilisateur {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private String motDePasse;
private String nom;
private String prenom;
private boolean actif = true;
@OneToMany(mappedBy = "proprietaire")
private List<
Photo> photos;
// Getters & Setters avec lombok
}

View File

@@ -0,0 +1,7 @@
package local.epul4a.fotosharing.repository;
import local.epul4a.fotosharing.model.Photo;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PhotoRepository extends JpaRepository<Photo, Long> {
}

View File

@@ -0,0 +1,10 @@
package local.epul4a.fotosharing.repository;
import local.epul4a.fotosharing.model.Utilisateur;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UtilisateurRepository extends JpaRepository<Utilisateur, Long> {
Optional<Utilisateur> findByEmail(String email);
}

View File

@@ -0,0 +1,43 @@
package local.epul4a.fotosharing.security;
import local.epul4a.fotosharing.model.Utilisateur;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
public class CustomUserDetails implements UserDetails {
private final Utilisateur user;
public CustomUserDetails(Utilisateur user) {
this.user = user;
}
public Utilisateur getUtilisateur() {
return user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// Si tu veux ajouter des rôles, adapte ici.
return Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public String getPassword() {
return user.getMotDePasse();
}
@Override
public String getUsername() {
return user.getEmail();
}
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return user.isActif(); }
}

View File

@@ -0,0 +1,24 @@
package local.epul4a.fotosharing.security;
import local.epul4a.fotosharing.model.Utilisateur;
import local.epul4a.fotosharing.repository.UtilisateurRepository;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UtilisateurRepository utilisateurRepository;
public CustomUserDetailsService(UtilisateurRepository utilisateurRepository) {
this.utilisateurRepository = utilisateurRepository;
}
@Override
public CustomUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Utilisateur u = utilisateurRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("Utilisateur introuvable: " + username));
return new CustomUserDetails(u);
}
}

View File

@@ -0,0 +1,63 @@
package local.epul4a.fotosharing.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
private final CustomUserDetailsService customUserDetailsService;
public SecurityConfig(CustomUserDetailsService customUserDetailsService) {
this.customUserDetailsService = customUserDetailsService;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/register", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/", true)
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout=true")
.permitAll()
)
.csrf(csrf -> csrf.disable());
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
// Spring Security 6.2 utilise ce bean automatiquement
return customUserDetailsService;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
// Spring Boot crée automatiquement un DaoAuthenticationProvider interne
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@@ -0,0 +1,30 @@
package local.epul4a.fotosharing.security;
import local.epul4a.fotosharing.model.Photo;
import local.epul4a.fotosharing.repository.PhotoRepository;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service("securityService")
public class SecurityService {
private final PhotoRepository photoRepository;
public SecurityService(PhotoRepository photoRepository) {
this.photoRepository = photoRepository;
}
public boolean canAccessPhoto(org.springframework.security.core.Authentication authentication, Long photoId) {
if (authentication == null || !authentication.isAuthenticated()) return false;
String username = authentication.getName(); // email
Optional<Photo> p = photoRepository.findById(photoId);
if (p.isEmpty()) return false;
Photo photo = p.get();
if (photo.getVisibilite() == Photo.Visibilite.PUBLIC) return true;
if (photo.getProprietaire() != null && photo.getProprietaire().getEmail().equals(username)) return true;
// TODO: vérifier table partage
return false;
}
}

View File

@@ -0,0 +1,11 @@
package local.epul4a.fotosharing.service;
import local.epul4a.fotosharing.model.Photo;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Path;
public interface PhotoService {
Photo store(MultipartFile file, String visibilite, String ownerEmail) throws IOException;
Path loadAsPath(String uuidFile);
}

View File

@@ -0,0 +1,61 @@
package local.epul4a.fotosharing.service.impl;
import local.epul4a.fotosharing.model.Photo;
import local.epul4a.fotosharing.model.Utilisateur;
import local.epul4a.fotosharing.repository.PhotoRepository;
import local.epul4a.fotosharing.repository.UtilisateurRepository;
import local.epul4a.fotosharing.service.PhotoService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.*;
import java.time.LocalDateTime;
import java.util.UUID;
@Service
public class PhotoServiceImpl implements PhotoService {
@Value("${file.upload-dir}")
private String uploadDir;
private final PhotoRepository photoRepository;
private final UtilisateurRepository utilisateurRepository;
public PhotoServiceImpl(PhotoRepository photoRepository, UtilisateurRepository utilisateurRepository) {
this.photoRepository = photoRepository;
this.utilisateurRepository = utilisateurRepository;
}
@Override
public Photo store(MultipartFile file, String visibilite, String ownerEmail) throws IOException {
if (file.isEmpty()) throw new IOException("Fichier vide");
// Vérifier taille / type si besoin (ici basique)
String original = StringUtils.cleanPath(file.getOriginalFilename());
String uuid = UUID.randomUUID().toString() + "-" + original;
Path uploadPath = Paths.get(uploadDir);
if (!Files.exists(uploadPath)) Files.createDirectories(uploadPath);
Path target = uploadPath.resolve(uuid);
Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);
Photo p = new Photo();
p.setNomFichierOriginal(original);
p.setUuidFichier(uuid);
p.setDateUpload(LocalDateTime.now());
p.setVisibilite(Photo.Visibilite.valueOf(visibilite));
Utilisateur u = utilisateurRepository.findByEmail(ownerEmail).orElseThrow(() -> new IOException("Utilisateur introuvable"));
p.setProprietaire(u);
return photoRepository.save(p);
}
@Override
public Path loadAsPath(String uuidFile) {
return Paths.get(uploadDir).resolve(uuidFile);
}
}

View File

@@ -3,7 +3,7 @@ spring.application.name=FotoSharing
# ===============================
# DATABASE
# ===============================
spring.datasource.url=jdbc:mariadb://192.168.124.171:3306/fotoshareDB
spring.datasource.url=jdbc:mariadb://192.168.112.10:3306/fotoshareDB
spring.datasource.username=ufoto
spring.datasource.password=4AinfoRep-25
# ===============================
@@ -12,6 +12,14 @@ spring.datasource.password=4AinfoRep-25
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML
spring.jpa.properties.hibernate.jdbc.time_zone=UTC
# ===============================
# EMPLACEMENT DE STICKAGE
# ===============================
file.upload-dir=/opt/photo-app/uploads
spring.servlet.multipart.max-file-size=20MB
spring.servlet.multipart.max-request-size=20MB

View File

@@ -0,0 +1,9 @@
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head><meta charset="utf-8"/><title>Accueil</title></head>
<body>
<h1>Bienvenue sur FotoSharing</h1>
<p><a th:href="@{/upload}">Uploader une photo</a></p>
<p><a th:href="@{/logout}">Se déconnecter</a></p>
</body>
</html>

View File

@@ -0,0 +1,23 @@
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<title>Login - FotoSharing</title>
</head>
<body>
<h1>Connexion</h1>
<form th:action="@{/login}" method="post">
<label>Email: <input type="text" name="username"/></label><br/>
<label>Mot de passe: <input type="password" name="password"/></label><br/>
<button type="submit">Se connecter</button>
</form>
<div th:if="${param.logout}">
Déconnecté avec succès.
</div>
<div th:if="${param.error}">
Erreur d'authentification.
</div>
<p><a th:href="@{/register}">Créer un compte</a></p>
</body>
</html>

View File

@@ -0,0 +1,17 @@
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<title>Inscription - FotoSharing</title>
</head>
<body>
<h1>Inscription</h1>
<form th:action="@{/register}" th:object="${utilisateur}" method="post">
<label>Email: <input th:field="*{email}" /></label><br/>
<label>Nom: <input th:field="*{nom}" /></label><br/>
<label>Prénom: <input th:field="*{prenom}" /></label><br/>
<label>Mot de passe: <input th:field="*{motDePasse}" type="password"/></label><br/>
<button type="submit">Créer</button>
</form>
</body>
</html>

View File

@@ -0,0 +1,23 @@
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<title>Upload - FotoSharing</title>
</head>
<body>
<h1>Uploader une photo</h1>
<form th:action="@{/upload}" method="post" enctype="multipart/form-data">
<input type="file" name="file"/><br/>
<label>Visibilité:
<select name="visibilite">
<option value="PRIVATE">Privée</option>
<option value="PUBLIC">Publique</option>
<option value="SHARED">Partagée</option>
</select>
</label><br/>
<button type="submit">Envoyer</button>
</form>
<div th:if="${error}" th:text="${error}"></div>
<div th:if="${message}" th:text="${message}"></div>
</body>
</html>

View File

@@ -3,7 +3,7 @@ spring.application.name=FotoSharing
# ===============================
# DATABASE
# ===============================
spring.datasource.url=jdbc:mariadb://192.168.124.171:3306/fotoshareDB
spring.datasource.url=jdbc:mariadb://192.168.112.10:3306/fotoshareDB
spring.datasource.username=ufoto
spring.datasource.password=4AinfoRep-25
# ===============================
@@ -12,6 +12,14 @@ spring.datasource.password=4AinfoRep-25
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML
spring.jpa.properties.hibernate.jdbc.time_zone=UTC
# ===============================
# EMPLACEMENT DE STICKAGE
# ===============================
file.upload-dir=/opt/photo-app/uploads
spring.servlet.multipart.max-file-size=20MB
spring.servlet.multipart.max-request-size=20MB

View File

@@ -0,0 +1,9 @@
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head><meta charset="utf-8"/><title>Accueil</title></head>
<body>
<h1>Bienvenue sur FotoSharing</h1>
<p><a th:href="@{/upload}">Uploader une photo</a></p>
<p><a th:href="@{/logout}">Se déconnecter</a></p>
</body>
</html>

View File

@@ -0,0 +1,23 @@
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<title>Login - FotoSharing</title>
</head>
<body>
<h1>Connexion</h1>
<form th:action="@{/login}" method="post">
<label>Email: <input type="text" name="username"/></label><br/>
<label>Mot de passe: <input type="password" name="password"/></label><br/>
<button type="submit">Se connecter</button>
</form>
<div th:if="${param.logout}">
Déconnecté avec succès.
</div>
<div th:if="${param.error}">
Erreur d'authentification.
</div>
<p><a th:href="@{/register}">Créer un compte</a></p>
</body>
</html>

View File

@@ -0,0 +1,17 @@
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<title>Inscription - FotoSharing</title>
</head>
<body>
<h1>Inscription</h1>
<form th:action="@{/register}" th:object="${utilisateur}" method="post">
<label>Email: <input th:field="*{email}" /></label><br/>
<label>Nom: <input th:field="*{nom}" /></label><br/>
<label>Prénom: <input th:field="*{prenom}" /></label><br/>
<label>Mot de passe: <input th:field="*{motDePasse}" type="password"/></label><br/>
<button type="submit">Créer</button>
</form>
</body>
</html>

View File

@@ -0,0 +1,23 @@
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<title>Upload - FotoSharing</title>
</head>
<body>
<h1>Uploader une photo</h1>
<form th:action="@{/upload}" method="post" enctype="multipart/form-data">
<input type="file" name="file"/><br/>
<label>Visibilité:
<select name="visibilite">
<option value="PRIVATE">Privée</option>
<option value="PUBLIC">Publique</option>
<option value="SHARED">Partagée</option>
</select>
</label><br/>
<button type="submit">Envoyer</button>
</form>
<div th:if="${error}" th:text="${error}"></div>
<div th:if="${message}" th:text="${message}"></div>
</body>
</html>