diff --git a/src/main/java/edu/ntnu/idatt210602/matsvinnbackend/controller/FileController.java b/src/main/java/edu/ntnu/idatt210602/matsvinnbackend/controller/FileController.java new file mode 100644 index 0000000000000000000000000000000000000000..382f0927bd872ccc87f5e6a76764dd4bc749571f --- /dev/null +++ b/src/main/java/edu/ntnu/idatt210602/matsvinnbackend/controller/FileController.java @@ -0,0 +1,154 @@ +package edu.ntnu.idatt210602.matsvinnbackend.controller; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.DeleteMapping; +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.RequestParam; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import edu.ntnu.idatt210602.matsvinnbackend.model.Account; +import edu.ntnu.idatt210602.matsvinnbackend.model.Profile; +import edu.ntnu.idatt210602.matsvinnbackend.repo.AccountRepository; +import edu.ntnu.idatt210602.matsvinnbackend.repo.ProfileRepository; + +import java.nio.file.Path; + +@Controller +@RequestMapping(path = "/img") +public class FileController { + + Logger logger = LoggerFactory.getLogger(ProfileController.class); + + @Value("${filebucket.path}") + String basePath; + + @Autowired + AccountRepository accountRepo; + + @Autowired + ProfileRepository profileRepo; + + @PostMapping("") + public ResponseEntity<String> uploadProfilePicture(@RequestParam("file") MultipartFile file, + @RequestParam("profileId") Integer profileId) { + String authenticatedUsername = SecurityContextHolder.getContext().getAuthentication().getName(); + Account loggedInAccount = accountRepo.findByEmail(authenticatedUsername).orElseThrow(); + + // Ensure that the provided profile ID is valid + Profile profile = profileRepo.findById(profileId).orElseThrow(() -> { + return new ResponseStatusException(HttpStatus.BAD_REQUEST); + }); + + // Ensure that the profile is part of the authenticated account + if (!loggedInAccount.getId().equals(profile.getAccountId())) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN); + } + + // Allow only JPEG images + if (!file.getContentType().equals(MediaType.IMAGE_JPEG_VALUE)) { + throw new ResponseStatusException(HttpStatus.UNSUPPORTED_MEDIA_TYPE); + } + + // Only allow images up to 512 kilobytes + if (file.getSize() > 524288) { + throw new ResponseStatusException(HttpStatus.PAYLOAD_TOO_LARGE); + } + + // Use unique profile ID as filename for 1:1 mapping between profiles and images + String filename = String.format("%d.jpeg", profileId); + Path path = Paths.get(basePath, filename); + + try { + Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + logger.error("Unable to write uploaded file to storage!"); + } + + String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath() + .path("/img/") + .path(profileId.toString()) + .toUriString(); + + return ResponseEntity.ok(fileDownloadUri); + } + + @GetMapping("/{profileId:.+}") + public ResponseEntity<Resource> get(@PathVariable Integer profileId) { + String authenticatedUsername = SecurityContextHolder.getContext().getAuthentication().getName(); + Account loggedInAccount = accountRepo.findByEmail(authenticatedUsername).orElseThrow(); + + // Ensure that the provided profile ID is valid + Profile profile = profileRepo.findById(profileId).orElseThrow(() -> { + return new ResponseStatusException(HttpStatus.BAD_REQUEST); + }); + + // Ensure that the profile is part of the authenticated account + if (!loggedInAccount.getId().equals(profile.getAccountId())) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN); + } + + Path path = Paths.get(basePath, String.format("%d.jpeg", profileId)); + + if (!path.toFile().exists()) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + } + + Resource file = null; + + try { + file = new UrlResource(path.toUri()); + } catch (Exception e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + } + + return ResponseEntity.ok() + .contentType(MediaType.IMAGE_JPEG) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFilename() + "\"") + .body(file); + } + + @DeleteMapping("") + public ResponseEntity<Void> deleteImage(@RequestParam("profileId") Integer profileId) { + String authenticatedUsername = SecurityContextHolder.getContext().getAuthentication().getName(); + Account loggedInAccount = accountRepo.findByEmail(authenticatedUsername).orElseThrow(); + + // Ensure that the provided profile ID is valid + Profile profile = profileRepo.findById(profileId).orElseThrow(() -> { + return new ResponseStatusException(HttpStatus.BAD_REQUEST); + }); + + // Ensure that the profile is part of the authenticated account + if (!loggedInAccount.getId().equals(profile.getAccountId())) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN); + } + + Path path = Paths.get(basePath, String.format("%d.jpeg", profileId)); + + if (path.toFile().delete()) { + return ResponseEntity.ok().build(); + } else { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idatt210602/matsvinnbackend/security/SecurityConfig.java b/src/main/java/edu/ntnu/idatt210602/matsvinnbackend/security/SecurityConfig.java index 2bc31e6b3d713da0162aadeee4ef6f45924d9f99..d744c4193d5c7babe645ac0949f06c4c54c2aa27 100644 --- a/src/main/java/edu/ntnu/idatt210602/matsvinnbackend/security/SecurityConfig.java +++ b/src/main/java/edu/ntnu/idatt210602/matsvinnbackend/security/SecurityConfig.java @@ -54,9 +54,10 @@ public class SecurityConfig { //FILE ENDPOINTS - /*.requestMatchers(HttpMethod.GET, "/files").permitAll() - .requestMatchers(HttpMethod.GET, "/files/*").permitAll() - .requestMatchers(HttpMethod.POST, "/files").permitAll()*/ + .requestMatchers(HttpMethod.GET, "/img").authenticated() + .requestMatchers(HttpMethod.GET, "/img/*").authenticated() + .requestMatchers(HttpMethod.POST, "/img").authenticated() + .requestMatchers(HttpMethod.DELETE, "/img/*").authenticated() .anyRequest().authenticated().and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ed5777a0ad8e9617d7e1f5a97fd6faed1ac3d486..cc8911137f79884da0f74426bd436e4bc7796ec5 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -5,3 +5,5 @@ spring.datasource.password=db spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver kassalapp.cache.url=http://${KASSALAPP_CACHE_HOST:localhost}:2730 + +filebucket.path=${FILESTORAGE_PATH:/tmp} \ No newline at end of file