From 4659aa44df2a2e1c885d398d0a15f6eeffbbae6c Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 25 Mar 2026 22:51:29 -0600 Subject: [PATCH] readd secure avatar endpoints --- android/.gitignore | 2 + android/app/.gitignore | 3 + backend/petshop-api.postman_collection.json | 75 ++++++++++++- .../config/FlywayContextInitializer.java | 38 +++++-- .../backend/controller/AuthController.java | 65 +++++------ .../controller/UserAvatarController.java | 43 +++++++ .../backend/service/AvatarStorageService.java | 105 ++++++++++++++++++ .../example/petshopdesktop/api/ApiClient.java | 25 +++++ .../petshopdesktop/api/endpoints/AuthApi.java | 4 + .../controllers/MainLayoutController.java | 47 ++++---- 10 files changed, 343 insertions(+), 64 deletions(-) create mode 100644 backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java create mode 100644 backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java diff --git a/android/.gitignore b/android/.gitignore index f7930e52..5cfb3b8d 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -16,6 +16,8 @@ /app/src/androidTest/ /app/src/test/ .DS_Store +/.project +/.settings/ /build /captures .externalNativeBuild diff --git a/android/app/.gitignore b/android/app/.gitignore index 12b09d99..86de5a5e 100644 --- a/android/app/.gitignore +++ b/android/app/.gitignore @@ -1,3 +1,6 @@ /build +/.classpath +/.project +/.settings/ /src/test/ /src/androidTest/ diff --git a/backend/petshop-api.postman_collection.json b/backend/petshop-api.postman_collection.json index d95a5b86..0d59e4ae 100644 --- a/backend/petshop-api.postman_collection.json +++ b/backend/petshop-api.postman_collection.json @@ -90,6 +90,10 @@ "key": "avatarFile", "value": "postman/avatar.png" }, + { + "key": "avatarUrl", + "value": "" + }, { "key": "bulkPetId", "value": "" @@ -212,6 +216,7 @@ " pm.response.to.have.status(200);", "});", "var jsonData = pm.response.json();", + "if (jsonData.id !== undefined) pm.collectionVariables.set('userId', jsonData.id);", "if (jsonData.token) pm.collectionVariables.set('customerToken', jsonData.token);" ] } @@ -307,7 +312,9 @@ "exec": [ "pm.test('Status code is 200', function () {", " 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);", "});", "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);", "});", "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/');", + "});" ] } } diff --git a/backend/src/main/java/com/petshop/backend/config/FlywayContextInitializer.java b/backend/src/main/java/com/petshop/backend/config/FlywayContextInitializer.java index 3fb28d10..000ebe86 100644 --- a/backend/src/main/java/com/petshop/backend/config/FlywayContextInitializer.java +++ b/backend/src/main/java/com/petshop/backend/config/FlywayContextInitializer.java @@ -10,6 +10,9 @@ import java.util.Arrays; public class FlywayContextInitializer implements ApplicationContextInitializer { + 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 !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; + } } } diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java index 2bd2b47d..b426aadc 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -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 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 error = new HashMap<>(); error.put("message", "No avatar uploaded"); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error); } Map response = new HashMap<>(); - response.put("avatarUrl", user.getAvatarUrl()); + response.put("avatarUrl", avatarStorageService.toOwnerAvatarUrl(user)); return ResponseEntity.ok(response); } + @GetMapping("/me/avatar/file") + public ResponseEntity 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); diff --git a/backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java b/backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java new file mode 100644 index 00000000..bb5c9342 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java @@ -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 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(); + } + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java b/backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java new file mode 100644 index 00000000..952ca600 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java @@ -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"; + }; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/ApiClient.java b/desktop/src/main/java/org/example/petshopdesktop/api/ApiClient.java index c0fbd874..ed2e9fc6 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/ApiClient.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/ApiClient.java @@ -48,6 +48,31 @@ public class ApiClient { 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 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 { HttpRequest.Builder builder = HttpRequest.newBuilder() .uri(URI.create(baseUrl + path)) diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/AuthApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/AuthApi.java index a273a738..0755ef9e 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/AuthApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/AuthApi.java @@ -26,6 +26,10 @@ public class AuthApi { 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 { apiClient.delete("/api/v1/auth/me/avatar"); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java index b262be0b..cb283dd2 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java @@ -18,7 +18,6 @@ import javafx.scene.paint.ImagePattern; import javafx.scene.shape.Circle; import javafx.stage.FileChooser; import javafx.stage.Stage; -import org.example.petshopdesktop.api.ApiConfig; import org.example.petshopdesktop.api.ChatRealtimeClient; import org.example.petshopdesktop.api.dto.auth.AvatarUploadResponse; 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.util.ActivityLogger; +import java.io.ByteArrayInputStream; + public class MainLayoutController { private static final String NAV_BASE_STYLE = "-fx-background-color: transparent; " + @@ -218,8 +219,7 @@ public class MainLayoutController { try { AvatarUploadResponse response = AuthApi.getInstance().uploadAvatar(file.toPath()); UserSession.getInstance().setAvatarUrl(response.getAvatarUrl()); - renderAvatar(UserSession.getInstance().getEmployeeName(), response.getAvatarUrl()); - btnRemoveAvatar.setDisable(response.getAvatarUrl() == null || response.getAvatarUrl().isBlank()); + refreshProfileHeader(); } catch (Exception e) { ActivityLogger.getInstance().logException("MainLayoutController.btnChangeAvatarClicked", e, "Uploading avatar"); showAvatarError(e.getMessage() != null ? e.getMessage() : "Could not upload profile picture."); @@ -263,7 +263,7 @@ public class MainLayoutController { @FXML public void initialize() { 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()); refreshProfileHeader(); applyRBAC(); @@ -285,20 +285,35 @@ public class MainLayoutController { String displayName = userInfo.getFullName() == null || userInfo.getFullName().isBlank() ? UserSession.getInstance().getUsername() : userInfo.getFullName(); + Image avatarImage = loadAvatarImage(userInfo.getAvatarUrl()); Platform.runLater(() -> { UserSession.getInstance().setEmployeeName(displayName); UserSession.getInstance().setAvatarUrl(userInfo.getAvatarUrl()); lblUsername.setText(displayName); - renderAvatar(displayName, userInfo.getAvatarUrl()); + renderAvatar(displayName, avatarImage); btnRemoveAvatar.setDisable(userInfo.getAvatarUrl() == null || userInfo.getAvatarUrl().isBlank()); }); } catch (Exception e) { - Platform.runLater(() -> renderAvatar(UserSession.getInstance().getEmployeeName(), UserSession.getInstance().getAvatarUrl())); + Platform.runLater(() -> renderAvatar(UserSession.getInstance().getEmployeeName(), null)); } }).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); border.setFill(Color.web("#dbe4ee")); @@ -306,21 +321,9 @@ public class MainLayoutController { Label initials = new Label(initials(displayName)); initials.setStyle("-fx-text-fill: white; -fx-font-weight: bold; -fx-font-size: 16px;"); - if (avatarUrl != null && !avatarUrl.isBlank()) { - try { - String resolvedUrl = avatarUrl.startsWith("http") ? avatarUrl : ApiConfig.getInstance().getBaseUrl() + avatarUrl; - 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); - } + if (avatarImage != null) { + circle.setFill(new ImagePattern(avatarImage)); + initials.setVisible(false); } else { circle.setFill(Color.web("#4ECDC4")); initials.setVisible(true);