readd secure avatar endpoints
This commit was merged in pull request #47.
This commit is contained in:
2
android/.gitignore
vendored
2
android/.gitignore
vendored
@@ -16,6 +16,8 @@
|
|||||||
/app/src/androidTest/
|
/app/src/androidTest/
|
||||||
/app/src/test/
|
/app/src/test/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
/.project
|
||||||
|
/.settings/
|
||||||
/build
|
/build
|
||||||
/captures
|
/captures
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
|
|||||||
3
android/app/.gitignore
vendored
3
android/app/.gitignore
vendored
@@ -1,3 +1,6 @@
|
|||||||
/build
|
/build
|
||||||
|
/.classpath
|
||||||
|
/.project
|
||||||
|
/.settings/
|
||||||
/src/test/
|
/src/test/
|
||||||
/src/androidTest/
|
/src/androidTest/
|
||||||
|
|||||||
@@ -90,6 +90,10 @@
|
|||||||
"key": "avatarFile",
|
"key": "avatarFile",
|
||||||
"value": "postman/avatar.png"
|
"value": "postman/avatar.png"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key": "avatarUrl",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "bulkPetId",
|
"key": "bulkPetId",
|
||||||
"value": ""
|
"value": ""
|
||||||
@@ -212,6 +216,7 @@
|
|||||||
" pm.response.to.have.status(200);",
|
" pm.response.to.have.status(200);",
|
||||||
"});",
|
"});",
|
||||||
"var jsonData = pm.response.json();",
|
"var jsonData = pm.response.json();",
|
||||||
|
"if (jsonData.id !== undefined) pm.collectionVariables.set('userId', jsonData.id);",
|
||||||
"if (jsonData.token) pm.collectionVariables.set('customerToken', jsonData.token);"
|
"if (jsonData.token) pm.collectionVariables.set('customerToken', jsonData.token);"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -307,7 +312,9 @@
|
|||||||
"exec": [
|
"exec": [
|
||||||
"pm.test('Status code is 200', function () {",
|
"pm.test('Status code is 200', function () {",
|
||||||
" pm.response.to.have.status(200);",
|
" pm.response.to.have.status(200);",
|
||||||
"});"
|
"});",
|
||||||
|
"var jsonData = pm.response.json();",
|
||||||
|
"if (jsonData.id !== undefined) pm.collectionVariables.set('userId', jsonData.id);"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -381,7 +388,8 @@
|
|||||||
" pm.response.to.have.status(200);",
|
" pm.response.to.have.status(200);",
|
||||||
"});",
|
"});",
|
||||||
"var jsonData = pm.response.json();",
|
"var jsonData = pm.response.json();",
|
||||||
"pm.expect(jsonData.avatarUrl).to.be.a('string');"
|
"pm.expect(jsonData.avatarUrl).to.be.a('string');",
|
||||||
|
"pm.collectionVariables.set('avatarUrl', jsonData.avatarUrl);"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -414,7 +422,68 @@
|
|||||||
" pm.response.to.have.status(200);",
|
" pm.response.to.have.status(200);",
|
||||||
"});",
|
"});",
|
||||||
"var jsonData = pm.response.json();",
|
"var jsonData = pm.response.json();",
|
||||||
"pm.expect(jsonData.avatarUrl).to.be.a('string');"
|
"pm.expect(jsonData.avatarUrl).to.be.a('string');",
|
||||||
|
"pm.collectionVariables.set('avatarUrl', jsonData.avatarUrl);"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get My Avatar File",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": "{{baseUrl}}{{avatarUrl}}",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{customerToken}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
"pm.test('Status code is 200', function () {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"pm.test('Avatar response is an image', function () {",
|
||||||
|
" pm.expect(pm.response.headers.get('Content-Type')).to.include('image/');",
|
||||||
|
"});"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get User Avatar File As Staff",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": "{{baseUrl}}/api/v1/users/{{userId}}/avatar/file",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{staffToken}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
"pm.test('Status code is 200', function () {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"pm.test('Avatar response is an image', function () {",
|
||||||
|
" pm.expect(pm.response.headers.get('Content-Type')).to.include('image/');",
|
||||||
|
"});"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import java.util.Arrays;
|
|||||||
|
|
||||||
public class FlywayContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
public class FlywayContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
||||||
|
|
||||||
|
private static final int MAX_RETRIES = 15;
|
||||||
|
private static final long RETRY_DELAY_MILLIS = 1000L;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(ConfigurableApplicationContext applicationContext) {
|
public void initialize(ConfigurableApplicationContext applicationContext) {
|
||||||
ConfigurableEnvironment environment = applicationContext.getEnvironment();
|
ConfigurableEnvironment environment = applicationContext.getEnvironment();
|
||||||
@@ -29,12 +32,33 @@ public class FlywayContextInitializer implements ApplicationContextInitializer<C
|
|||||||
.filter(location -> !location.isEmpty())
|
.filter(location -> !location.isEmpty())
|
||||||
.toArray(String[]::new);
|
.toArray(String[]::new);
|
||||||
|
|
||||||
Flyway.configure()
|
RuntimeException lastFailure = null;
|
||||||
.dataSource(url, username, password)
|
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||||
.locations(locations)
|
try {
|
||||||
.baselineOnMigrate(environment.getProperty("spring.flyway.baseline-on-migrate", Boolean.class, false))
|
Flyway.configure()
|
||||||
.baselineVersion(MigrationVersion.fromVersion(environment.getProperty("spring.flyway.baseline-version", "1")))
|
.dataSource(url, username, password)
|
||||||
.load()
|
.locations(locations)
|
||||||
.migrate();
|
.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.EmployeeStoreRepository;
|
||||||
import com.petshop.backend.repository.UserRepository;
|
import com.petshop.backend.repository.UserRepository;
|
||||||
import com.petshop.backend.security.JwtUtil;
|
import com.petshop.backend.security.JwtUtil;
|
||||||
|
import com.petshop.backend.service.AvatarStorageService;
|
||||||
import com.petshop.backend.service.UserBusinessLinkageService;
|
import com.petshop.backend.service.UserBusinessLinkageService;
|
||||||
import com.petshop.backend.util.AuthenticationHelper;
|
import com.petshop.backend.util.AuthenticationHelper;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
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.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
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.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/auth")
|
@RequestMapping("/api/v1/auth")
|
||||||
@@ -49,8 +46,9 @@ public class AuthController {
|
|||||||
private final UserBusinessLinkageService userBusinessLinkageService;
|
private final UserBusinessLinkageService userBusinessLinkageService;
|
||||||
private final EmployeeRepository employeeRepository;
|
private final EmployeeRepository employeeRepository;
|
||||||
private final EmployeeStoreRepository employeeStoreRepository;
|
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.authenticationManager = authenticationManager;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.jwtUtil = jwtUtil;
|
this.jwtUtil = jwtUtil;
|
||||||
@@ -58,6 +56,7 @@ public class AuthController {
|
|||||||
this.userBusinessLinkageService = userBusinessLinkageService;
|
this.userBusinessLinkageService = userBusinessLinkageService;
|
||||||
this.employeeRepository = employeeRepository;
|
this.employeeRepository = employeeRepository;
|
||||||
this.employeeStoreRepository = employeeStoreRepository;
|
this.employeeStoreRepository = employeeStoreRepository;
|
||||||
|
this.avatarStorageService = avatarStorageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
@@ -155,7 +154,7 @@ public class AuthController {
|
|||||||
user.getEmail(),
|
user.getEmail(),
|
||||||
user.getFullName(),
|
user.getFullName(),
|
||||||
user.getPhone(),
|
user.getPhone(),
|
||||||
user.getAvatarUrl(),
|
avatarStorageService.toOwnerAvatarUrl(user),
|
||||||
user.getRole().name(),
|
user.getRole().name(),
|
||||||
employeeStore != null ? employeeStore.getStore().getStoreId() : null,
|
employeeStore != null ? employeeStore.getStore().getStoreId() : null,
|
||||||
employeeStore != null ? employeeStore.getStore().getStoreName() : null
|
employeeStore != null ? employeeStore.getStore().getStoreName() : null
|
||||||
@@ -224,7 +223,7 @@ public class AuthController {
|
|||||||
updatedUser.getEmail(),
|
updatedUser.getEmail(),
|
||||||
updatedUser.getFullName(),
|
updatedUser.getFullName(),
|
||||||
updatedUser.getPhone(),
|
updatedUser.getPhone(),
|
||||||
updatedUser.getAvatarUrl(),
|
avatarStorageService.toOwnerAvatarUrl(updatedUser),
|
||||||
updatedUser.getRole().name(),
|
updatedUser.getRole().name(),
|
||||||
employeeStore != null ? employeeStore.getStore().getStoreId() : null,
|
employeeStore != null ? employeeStore.getStore().getStoreId() : null,
|
||||||
employeeStore != null ? employeeStore.getStore().getStoreName() : null
|
employeeStore != null ? employeeStore.getStore().getStoreName() : null
|
||||||
@@ -273,26 +272,12 @@ public class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String uploadDir = "uploads/avatars";
|
avatarStorageService.deleteAvatar(user);
|
||||||
File directory = new File(uploadDir);
|
String avatarPath = avatarStorageService.storeAvatar(file);
|
||||||
if (!directory.exists()) {
|
user.setAvatarUrl(avatarPath);
|
||||||
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);
|
|
||||||
userRepository.save(user);
|
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) {
|
} catch (IOException e) {
|
||||||
Map<String, String> error = new HashMap<>();
|
Map<String, String> error = new HashMap<>();
|
||||||
@@ -305,25 +290,41 @@ public class AuthController {
|
|||||||
public ResponseEntity<?> getAvatar() {
|
public ResponseEntity<?> getAvatar() {
|
||||||
User user = getAuthenticatedUser();
|
User user = getAuthenticatedUser();
|
||||||
|
|
||||||
if (user.getAvatarUrl() == null || user.getAvatarUrl().isEmpty()) {
|
if (!avatarStorageService.hasAvatar(user)) {
|
||||||
Map<String, String> error = new HashMap<>();
|
Map<String, String> error = new HashMap<>();
|
||||||
error.put("message", "No avatar uploaded");
|
error.put("message", "No avatar uploaded");
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> response = new HashMap<>();
|
Map<String, String> response = new HashMap<>();
|
||||||
response.put("avatarUrl", user.getAvatarUrl());
|
response.put("avatarUrl", avatarStorageService.toOwnerAvatarUrl(user));
|
||||||
return ResponseEntity.ok(response);
|
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")
|
@DeleteMapping("/me/avatar")
|
||||||
public ResponseEntity<?> deleteAvatar() {
|
public ResponseEntity<?> deleteAvatar() {
|
||||||
User user = getAuthenticatedUser();
|
User user = getAuthenticatedUser();
|
||||||
|
|
||||||
if (user.getAvatarUrl() != null && !user.getAvatarUrl().isEmpty()) {
|
if (avatarStorageService.hasAvatar(user)) {
|
||||||
try {
|
try {
|
||||||
Path filePath = Paths.get("." + user.getAvatarUrl());
|
avatarStorageService.deleteAvatar(user);
|
||||||
Files.deleteIfExists(filePath);
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
}
|
}
|
||||||
user.setAvatarUrl(null);
|
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";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,31 @@ public class ApiClient {
|
|||||||
return handleResponse(response, responseClass);
|
return handleResponse(response, responseClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] getBytes(String path) throws Exception {
|
||||||
|
HttpRequest.Builder builder = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + path))
|
||||||
|
.GET()
|
||||||
|
.timeout(Duration.ofSeconds(30));
|
||||||
|
|
||||||
|
addAuthHeader(builder);
|
||||||
|
|
||||||
|
HttpRequest request = builder.build();
|
||||||
|
HttpResponse<byte[]> response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
|
||||||
|
|
||||||
|
int statusCode = response.statusCode();
|
||||||
|
if (statusCode == 200 || statusCode == 201) {
|
||||||
|
return response.body();
|
||||||
|
} else if (statusCode == 401) {
|
||||||
|
throw new RuntimeException("Authentication failed. Please log in again.");
|
||||||
|
} else if (statusCode == 403) {
|
||||||
|
throw new RuntimeException("Access restricted. You don't have permission to perform this action.");
|
||||||
|
} else if (statusCode == 404) {
|
||||||
|
throw new RuntimeException("Avatar not found.");
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Request failed with status " + statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public String getRawResponse(String path) throws Exception {
|
public String getRawResponse(String path) throws Exception {
|
||||||
HttpRequest.Builder builder = HttpRequest.newBuilder()
|
HttpRequest.Builder builder = HttpRequest.newBuilder()
|
||||||
.uri(URI.create(baseUrl + path))
|
.uri(URI.create(baseUrl + path))
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ public class AuthApi {
|
|||||||
return apiClient.postMultipart("/api/v1/auth/me/avatar", "avatar", filePath, AvatarUploadResponse.class);
|
return apiClient.postMultipart("/api/v1/auth/me/avatar", "avatar", filePath, AvatarUploadResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] getMyAvatarFile() throws Exception {
|
||||||
|
return apiClient.getBytes("/api/v1/auth/me/avatar/file");
|
||||||
|
}
|
||||||
|
|
||||||
public void deleteAvatar() throws Exception {
|
public void deleteAvatar() throws Exception {
|
||||||
apiClient.delete("/api/v1/auth/me/avatar");
|
apiClient.delete("/api/v1/auth/me/avatar");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import javafx.scene.paint.ImagePattern;
|
|||||||
import javafx.scene.shape.Circle;
|
import javafx.scene.shape.Circle;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.example.petshopdesktop.api.ApiConfig;
|
|
||||||
import org.example.petshopdesktop.api.ChatRealtimeClient;
|
import org.example.petshopdesktop.api.ChatRealtimeClient;
|
||||||
import org.example.petshopdesktop.api.dto.auth.AvatarUploadResponse;
|
import org.example.petshopdesktop.api.dto.auth.AvatarUploadResponse;
|
||||||
import org.example.petshopdesktop.api.dto.auth.UserInfoResponse;
|
import org.example.petshopdesktop.api.dto.auth.UserInfoResponse;
|
||||||
@@ -27,6 +26,8 @@ import org.example.petshopdesktop.auth.UserSession;
|
|||||||
import org.example.petshopdesktop.ui.SvgWebViewFactory;
|
import org.example.petshopdesktop.ui.SvgWebViewFactory;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
|
||||||
public class MainLayoutController {
|
public class MainLayoutController {
|
||||||
|
|
||||||
private static final String NAV_BASE_STYLE = "-fx-background-color: transparent; " +
|
private static final String NAV_BASE_STYLE = "-fx-background-color: transparent; " +
|
||||||
@@ -218,8 +219,7 @@ public class MainLayoutController {
|
|||||||
try {
|
try {
|
||||||
AvatarUploadResponse response = AuthApi.getInstance().uploadAvatar(file.toPath());
|
AvatarUploadResponse response = AuthApi.getInstance().uploadAvatar(file.toPath());
|
||||||
UserSession.getInstance().setAvatarUrl(response.getAvatarUrl());
|
UserSession.getInstance().setAvatarUrl(response.getAvatarUrl());
|
||||||
renderAvatar(UserSession.getInstance().getEmployeeName(), response.getAvatarUrl());
|
refreshProfileHeader();
|
||||||
btnRemoveAvatar.setDisable(response.getAvatarUrl() == null || response.getAvatarUrl().isBlank());
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ActivityLogger.getInstance().logException("MainLayoutController.btnChangeAvatarClicked", e, "Uploading avatar");
|
ActivityLogger.getInstance().logException("MainLayoutController.btnChangeAvatarClicked", e, "Uploading avatar");
|
||||||
showAvatarError(e.getMessage() != null ? e.getMessage() : "Could not upload profile picture.");
|
showAvatarError(e.getMessage() != null ? e.getMessage() : "Could not upload profile picture.");
|
||||||
@@ -263,7 +263,7 @@ public class MainLayoutController {
|
|||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
logoContainer.getChildren().setAll(SvgWebViewFactory.build("/org/example/petshopdesktop/images/leons-pet-store-badge-light.svg", 94));
|
logoContainer.getChildren().setAll(SvgWebViewFactory.build("/org/example/petshopdesktop/images/leons-pet-store-badge-light.svg", 94));
|
||||||
renderAvatar(UserSession.getInstance().getEmployeeName(), UserSession.getInstance().getAvatarUrl());
|
renderAvatar(UserSession.getInstance().getEmployeeName(), null);
|
||||||
btnRemoveAvatar.setDisable(UserSession.getInstance().getAvatarUrl() == null || UserSession.getInstance().getAvatarUrl().isBlank());
|
btnRemoveAvatar.setDisable(UserSession.getInstance().getAvatarUrl() == null || UserSession.getInstance().getAvatarUrl().isBlank());
|
||||||
refreshProfileHeader();
|
refreshProfileHeader();
|
||||||
applyRBAC();
|
applyRBAC();
|
||||||
@@ -285,20 +285,35 @@ public class MainLayoutController {
|
|||||||
String displayName = userInfo.getFullName() == null || userInfo.getFullName().isBlank()
|
String displayName = userInfo.getFullName() == null || userInfo.getFullName().isBlank()
|
||||||
? UserSession.getInstance().getUsername()
|
? UserSession.getInstance().getUsername()
|
||||||
: userInfo.getFullName();
|
: userInfo.getFullName();
|
||||||
|
Image avatarImage = loadAvatarImage(userInfo.getAvatarUrl());
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
UserSession.getInstance().setEmployeeName(displayName);
|
UserSession.getInstance().setEmployeeName(displayName);
|
||||||
UserSession.getInstance().setAvatarUrl(userInfo.getAvatarUrl());
|
UserSession.getInstance().setAvatarUrl(userInfo.getAvatarUrl());
|
||||||
lblUsername.setText(displayName);
|
lblUsername.setText(displayName);
|
||||||
renderAvatar(displayName, userInfo.getAvatarUrl());
|
renderAvatar(displayName, avatarImage);
|
||||||
btnRemoveAvatar.setDisable(userInfo.getAvatarUrl() == null || userInfo.getAvatarUrl().isBlank());
|
btnRemoveAvatar.setDisable(userInfo.getAvatarUrl() == null || userInfo.getAvatarUrl().isBlank());
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Platform.runLater(() -> renderAvatar(UserSession.getInstance().getEmployeeName(), UserSession.getInstance().getAvatarUrl()));
|
Platform.runLater(() -> renderAvatar(UserSession.getInstance().getEmployeeName(), null));
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renderAvatar(String displayName, String avatarUrl) {
|
private Image loadAvatarImage(String avatarUrl) {
|
||||||
|
if (avatarUrl == null || avatarUrl.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] imageBytes = AuthApi.getInstance().getMyAvatarFile();
|
||||||
|
Image image = new Image(new ByteArrayInputStream(imageBytes), 52, 52, true, true);
|
||||||
|
return image.isError() ? null : image;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderAvatar(String displayName, Image avatarImage) {
|
||||||
Circle border = new Circle(29);
|
Circle border = new Circle(29);
|
||||||
border.setFill(Color.web("#dbe4ee"));
|
border.setFill(Color.web("#dbe4ee"));
|
||||||
|
|
||||||
@@ -306,21 +321,9 @@ public class MainLayoutController {
|
|||||||
Label initials = new Label(initials(displayName));
|
Label initials = new Label(initials(displayName));
|
||||||
initials.setStyle("-fx-text-fill: white; -fx-font-weight: bold; -fx-font-size: 16px;");
|
initials.setStyle("-fx-text-fill: white; -fx-font-weight: bold; -fx-font-size: 16px;");
|
||||||
|
|
||||||
if (avatarUrl != null && !avatarUrl.isBlank()) {
|
if (avatarImage != null) {
|
||||||
try {
|
circle.setFill(new ImagePattern(avatarImage));
|
||||||
String resolvedUrl = avatarUrl.startsWith("http") ? avatarUrl : ApiConfig.getInstance().getBaseUrl() + avatarUrl;
|
initials.setVisible(false);
|
||||||
Image image = new Image(resolvedUrl, 52, 52, true, true, true);
|
|
||||||
if (!image.isError()) {
|
|
||||||
circle.setFill(new ImagePattern(image));
|
|
||||||
initials.setVisible(false);
|
|
||||||
} else {
|
|
||||||
circle.setFill(Color.web("#4ECDC4"));
|
|
||||||
initials.setVisible(true);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
circle.setFill(Color.web("#4ECDC4"));
|
|
||||||
initials.setVisible(true);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
circle.setFill(Color.web("#4ECDC4"));
|
circle.setFill(Color.web("#4ECDC4"));
|
||||||
initials.setVisible(true);
|
initials.setVisible(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user