readd secure avatar endpoints
This commit is contained in:
@@ -10,6 +10,9 @@ import java.util.Arrays;
|
||||
|
||||
public class FlywayContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
||||
|
||||
private static final int MAX_RETRIES = 15;
|
||||
private static final long RETRY_DELAY_MILLIS = 1000L;
|
||||
|
||||
@Override
|
||||
public void initialize(ConfigurableApplicationContext applicationContext) {
|
||||
ConfigurableEnvironment environment = applicationContext.getEnvironment();
|
||||
@@ -29,12 +32,33 @@ public class FlywayContextInitializer implements ApplicationContextInitializer<C
|
||||
.filter(location -> !location.isEmpty())
|
||||
.toArray(String[]::new);
|
||||
|
||||
Flyway.configure()
|
||||
.dataSource(url, username, password)
|
||||
.locations(locations)
|
||||
.baselineOnMigrate(environment.getProperty("spring.flyway.baseline-on-migrate", Boolean.class, false))
|
||||
.baselineVersion(MigrationVersion.fromVersion(environment.getProperty("spring.flyway.baseline-version", "1")))
|
||||
.load()
|
||||
.migrate();
|
||||
RuntimeException lastFailure = null;
|
||||
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
Flyway.configure()
|
||||
.dataSource(url, username, password)
|
||||
.locations(locations)
|
||||
.baselineOnMigrate(environment.getProperty("spring.flyway.baseline-on-migrate", Boolean.class, false))
|
||||
.baselineVersion(MigrationVersion.fromVersion(environment.getProperty("spring.flyway.baseline-version", "1")))
|
||||
.load()
|
||||
.migrate();
|
||||
return;
|
||||
} catch (RuntimeException ex) {
|
||||
lastFailure = ex;
|
||||
if (attempt == MAX_RETRIES) {
|
||||
throw ex;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(RETRY_DELAY_MILLIS);
|
||||
} catch (InterruptedException interruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Interrupted while waiting for database startup", interruptedException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastFailure != null) {
|
||||
throw lastFailure;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,13 @@ import com.petshop.backend.repository.EmployeeRepository;
|
||||
import com.petshop.backend.repository.EmployeeStoreRepository;
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
import com.petshop.backend.security.JwtUtil;
|
||||
import com.petshop.backend.service.AvatarStorageService;
|
||||
import com.petshop.backend.service.UserBusinessLinkageService;
|
||||
import com.petshop.backend.util.AuthenticationHelper;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
@@ -28,15 +31,9 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth")
|
||||
@@ -49,8 +46,9 @@ public class AuthController {
|
||||
private final UserBusinessLinkageService userBusinessLinkageService;
|
||||
private final EmployeeRepository employeeRepository;
|
||||
private final EmployeeStoreRepository employeeStoreRepository;
|
||||
private final AvatarStorageService avatarStorageService;
|
||||
|
||||
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository) {
|
||||
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository, AvatarStorageService avatarStorageService) {
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.userRepository = userRepository;
|
||||
this.jwtUtil = jwtUtil;
|
||||
@@ -58,6 +56,7 @@ public class AuthController {
|
||||
this.userBusinessLinkageService = userBusinessLinkageService;
|
||||
this.employeeRepository = employeeRepository;
|
||||
this.employeeStoreRepository = employeeStoreRepository;
|
||||
this.avatarStorageService = avatarStorageService;
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
@@ -155,7 +154,7 @@ public class AuthController {
|
||||
user.getEmail(),
|
||||
user.getFullName(),
|
||||
user.getPhone(),
|
||||
user.getAvatarUrl(),
|
||||
avatarStorageService.toOwnerAvatarUrl(user),
|
||||
user.getRole().name(),
|
||||
employeeStore != null ? employeeStore.getStore().getStoreId() : null,
|
||||
employeeStore != null ? employeeStore.getStore().getStoreName() : null
|
||||
@@ -224,7 +223,7 @@ public class AuthController {
|
||||
updatedUser.getEmail(),
|
||||
updatedUser.getFullName(),
|
||||
updatedUser.getPhone(),
|
||||
updatedUser.getAvatarUrl(),
|
||||
avatarStorageService.toOwnerAvatarUrl(updatedUser),
|
||||
updatedUser.getRole().name(),
|
||||
employeeStore != null ? employeeStore.getStore().getStoreId() : null,
|
||||
employeeStore != null ? employeeStore.getStore().getStoreName() : null
|
||||
@@ -273,26 +272,12 @@ public class AuthController {
|
||||
}
|
||||
|
||||
try {
|
||||
String uploadDir = "uploads/avatars";
|
||||
File directory = new File(uploadDir);
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs();
|
||||
}
|
||||
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
String extension = originalFilename != null && originalFilename.contains(".")
|
||||
? originalFilename.substring(originalFilename.lastIndexOf("."))
|
||||
: ".jpg";
|
||||
String filename = UUID.randomUUID().toString() + extension;
|
||||
Path filePath = Paths.get(uploadDir, filename);
|
||||
|
||||
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
String avatarUrl = "/uploads/avatars/" + filename;
|
||||
user.setAvatarUrl(avatarUrl);
|
||||
avatarStorageService.deleteAvatar(user);
|
||||
String avatarPath = avatarStorageService.storeAvatar(file);
|
||||
user.setAvatarUrl(avatarPath);
|
||||
userRepository.save(user);
|
||||
|
||||
return ResponseEntity.ok(new AvatarUploadResponse(avatarUrl, "Avatar uploaded successfully"));
|
||||
return ResponseEntity.ok(new AvatarUploadResponse(avatarStorageService.toOwnerAvatarUrl(user), "Avatar uploaded successfully"));
|
||||
|
||||
} catch (IOException e) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
@@ -305,25 +290,41 @@ public class AuthController {
|
||||
public ResponseEntity<?> getAvatar() {
|
||||
User user = getAuthenticatedUser();
|
||||
|
||||
if (user.getAvatarUrl() == null || user.getAvatarUrl().isEmpty()) {
|
||||
if (!avatarStorageService.hasAvatar(user)) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("message", "No avatar uploaded");
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
|
||||
}
|
||||
|
||||
Map<String, String> response = new HashMap<>();
|
||||
response.put("avatarUrl", user.getAvatarUrl());
|
||||
response.put("avatarUrl", avatarStorageService.toOwnerAvatarUrl(user));
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/me/avatar/file")
|
||||
public ResponseEntity<Resource> getAvatarFile() {
|
||||
User user = getAuthenticatedUser();
|
||||
|
||||
if (!avatarStorageService.hasAvatar(user)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
try {
|
||||
Resource resource = avatarStorageService.loadAvatarResource(user);
|
||||
MediaType mediaType = avatarStorageService.resolveMediaType(user);
|
||||
return ResponseEntity.ok().contentType(mediaType).body(resource);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/me/avatar")
|
||||
public ResponseEntity<?> deleteAvatar() {
|
||||
User user = getAuthenticatedUser();
|
||||
|
||||
if (user.getAvatarUrl() != null && !user.getAvatarUrl().isEmpty()) {
|
||||
if (avatarStorageService.hasAvatar(user)) {
|
||||
try {
|
||||
Path filePath = Paths.get("." + user.getAvatarUrl());
|
||||
Files.deleteIfExists(filePath);
|
||||
avatarStorageService.deleteAvatar(user);
|
||||
} catch (IOException e) {
|
||||
}
|
||||
user.setAvatarUrl(null);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.petshop.backend.controller;
|
||||
|
||||
import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
import com.petshop.backend.service.AvatarStorageService;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/users")
|
||||
public class UserAvatarController {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final AvatarStorageService avatarStorageService;
|
||||
|
||||
public UserAvatarController(UserRepository userRepository, AvatarStorageService avatarStorageService) {
|
||||
this.userRepository = userRepository;
|
||||
this.avatarStorageService = avatarStorageService;
|
||||
}
|
||||
|
||||
@GetMapping("/{userId}/avatar/file")
|
||||
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
|
||||
public ResponseEntity<Resource> getUserAvatarFile(@PathVariable Long userId) {
|
||||
User user = userRepository.findById(userId).orElse(null);
|
||||
if (user == null || !avatarStorageService.hasAvatar(user)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
try {
|
||||
Resource resource = avatarStorageService.loadAvatarResource(user);
|
||||
return ResponseEntity.ok()
|
||||
.contentType(avatarStorageService.resolveMediaType(user))
|
||||
.body(resource);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.petshop.backend.service;
|
||||
|
||||
import com.petshop.backend.entity.User;
|
||||
import org.springframework.core.io.PathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.MediaTypeFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class AvatarStorageService {
|
||||
|
||||
private static final String STORED_PREFIX = "/uploads/avatars/";
|
||||
private static final String OWNER_ENDPOINT = "/api/v1/auth/me/avatar/file";
|
||||
|
||||
private final Path avatarDirectory = Paths.get("uploads", "avatars").toAbsolutePath().normalize();
|
||||
|
||||
public String storeAvatar(MultipartFile file) throws IOException {
|
||||
Files.createDirectories(avatarDirectory);
|
||||
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
String extension = resolveExtension(originalFilename);
|
||||
String filename = UUID.randomUUID() + extension;
|
||||
Path filePath = avatarDirectory.resolve(filename).normalize();
|
||||
|
||||
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
return STORED_PREFIX + filename;
|
||||
}
|
||||
|
||||
public Resource loadAvatarResource(User user) {
|
||||
Path filePath = resolveStoredAvatarPath(user.getAvatarUrl());
|
||||
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
|
||||
throw new IllegalArgumentException("Avatar file was not found");
|
||||
}
|
||||
return new PathResource(filePath);
|
||||
}
|
||||
|
||||
public void deleteAvatar(User user) throws IOException {
|
||||
if (user.getAvatarUrl() == null || user.getAvatarUrl().isBlank()) {
|
||||
return;
|
||||
}
|
||||
Files.deleteIfExists(resolveStoredAvatarPath(user.getAvatarUrl()));
|
||||
}
|
||||
|
||||
public String toOwnerAvatarUrl(User user) {
|
||||
return hasAvatar(user) ? OWNER_ENDPOINT : null;
|
||||
}
|
||||
|
||||
public String toStoredAvatarUrl(String avatarFilenamePath) {
|
||||
return avatarFilenamePath;
|
||||
}
|
||||
|
||||
public boolean hasAvatar(User user) {
|
||||
return user.getAvatarUrl() != null && !user.getAvatarUrl().isBlank();
|
||||
}
|
||||
|
||||
public MediaType resolveMediaType(User user) {
|
||||
try {
|
||||
return MediaTypeFactory.getMediaType(loadAvatarResource(user)).orElse(MediaType.APPLICATION_OCTET_STREAM);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return MediaType.APPLICATION_OCTET_STREAM;
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolveStoredAvatarPath(String storedAvatarUrl) {
|
||||
if (storedAvatarUrl == null || storedAvatarUrl.isBlank() || !storedAvatarUrl.startsWith(STORED_PREFIX)) {
|
||||
throw new IllegalArgumentException("Avatar file was not found");
|
||||
}
|
||||
|
||||
String filename = storedAvatarUrl.substring(STORED_PREFIX.length());
|
||||
if (filename.isBlank() || filename.contains("/") || filename.contains("\\") || filename.contains("..")) {
|
||||
throw new IllegalArgumentException("Avatar file was not found");
|
||||
}
|
||||
|
||||
Path resolved = avatarDirectory.resolve(filename).normalize();
|
||||
if (!resolved.startsWith(avatarDirectory)) {
|
||||
throw new IllegalArgumentException("Avatar file was not found");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private String resolveExtension(String originalFilename) {
|
||||
if (originalFilename == null) {
|
||||
return ".jpg";
|
||||
}
|
||||
int extensionIndex = originalFilename.lastIndexOf('.');
|
||||
if (extensionIndex < 0 || extensionIndex == originalFilename.length() - 1) {
|
||||
return ".jpg";
|
||||
}
|
||||
String extension = originalFilename.substring(extensionIndex).toLowerCase(Locale.ROOT);
|
||||
return switch (extension) {
|
||||
case ".jpg", ".jpeg", ".png", ".gif" -> extension;
|
||||
default -> ".jpg";
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user