diff --git a/backend/src/main/java/de/unipassau/fim/fsinfo/prost/controller/ShopController.java b/backend/src/main/java/de/unipassau/fim/fsinfo/prost/controller/ShopController.java index 60395a8..98d0641 100644 --- a/backend/src/main/java/de/unipassau/fim/fsinfo/prost/controller/ShopController.java +++ b/backend/src/main/java/de/unipassau/fim/fsinfo/prost/controller/ShopController.java @@ -16,6 +16,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.FileSystemResource; 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.Authentication; @@ -23,6 +24,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -65,18 +67,30 @@ public ResponseEntity> get(@RequestParam String id) { } @GetMapping("/item/picture") - public ResponseEntity getDisplayImage(@RequestParam String id) { + public ResponseEntity getDisplayImage(@RequestParam String id, + @RequestHeader(value = HttpHeaders.IF_MODIFIED_SINCE, required = false) String ifModifiedSince) { Optional item = itemRepository.findById(id); if (item.isPresent()) { - Optional file = fileStorageService.getItemPicture(item.get()); - if (file.isPresent()) { - FileSystemResource resource = new FileSystemResource(file.get()); + Optional fileO = fileStorageService.getItemPicture(item.get()); + if (fileO.isPresent()) { + File file = fileO.get(); + long lastModified = file.lastModified(); + + // Handle last modified check + if (ifModifiedSince != null && Long.parseLong(ifModifiedSince) >= lastModified) { + return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build(); + } + + FileSystemResource resource = new FileSystemResource(file); return ResponseEntity.ok() .contentType(MediaType.APPLICATION_OCTET_STREAM) .header(HttpHeaders.CONTENT_DISPOSITION, - "attachment; filename=\"" + file.get().getName() + "\"") + "attachment; filename=\"" + file.getName() + "\"") + .header(HttpHeaders.CACHE_CONTROL, "max-age=31536000, public, immutable") + .header(HttpHeaders.ETAG, String.valueOf(file.lastModified())) + .header(HttpHeaders.LAST_MODIFIED, String.valueOf(file.lastModified())) .body(resource); } else { return ResponseEntity.noContent().build(); diff --git a/backend/src/main/java/de/unipassau/fim/fsinfo/prost/security/WebConfig.java b/backend/src/main/java/de/unipassau/fim/fsinfo/prost/security/WebConfig.java index 5b26d1d..ab1a6ae 100644 --- a/backend/src/main/java/de/unipassau/fim/fsinfo/prost/security/WebConfig.java +++ b/backend/src/main/java/de/unipassau/fim/fsinfo/prost/security/WebConfig.java @@ -33,7 +33,7 @@ public class WebConfig implements WebMvcConfigurer { public static final String[] AUTH_WHITELIST = { "/api/authentication", - "/api/statistics/metric/**", + "/api/shop/item/picture", }; public static final String[] USER_SPACE = { @@ -44,6 +44,7 @@ public class WebConfig implements WebMvcConfigurer { "/api/transaction/me", "/api/invoice/me", "/api/shop/item/**", + "/api/statistics/metric/**", }; public static final String[] KIOSK_SPACE = { diff --git a/frontend/src/Queries.ts b/frontend/src/Queries.ts index 9d67f7a..dd24ffc 100644 --- a/frontend/src/Queries.ts +++ b/frontend/src/Queries.ts @@ -1,12 +1,20 @@ import axios from "axios"; -import { ShopHistoryEntryPage } from "./Types/ShopHistory"; -import { ShopItem } from "./Types/ShopItem"; -import { AuthorizedUser, User } from "./Types/User"; -import { getEncodedCredentials, setAuthorizedUser } from "./SessionInfo"; -import { InvoicePage } from "./Types/Invoice"; -import { TransactionPage } from "./Types/Transaction"; -import { UserMetricType as UserMetricType, UserMetricEntry as UserMetricEntry, TimeSpan, ItemMetricType as ItemMetricType, ItemMetricEntry as ItemMetricEntry, CompositeMetricEntry as CompositeMetricEntry, CompositeMetricType as CompositeMetricType } from "./Types/Statistics"; -import { toast } from "react-toastify"; +import {ShopHistoryEntryPage} from "./Types/ShopHistory"; +import {ShopItem} from "./Types/ShopItem"; +import {AuthorizedUser, User} from "./Types/User"; +import {getEncodedCredentials, setAuthorizedUser} from "./SessionInfo"; +import {InvoicePage} from "./Types/Invoice"; +import {TransactionPage} from "./Types/Transaction"; +import { + CompositeMetricEntry as CompositeMetricEntry, + CompositeMetricType as CompositeMetricType, + ItemMetricEntry as ItemMetricEntry, + ItemMetricType as ItemMetricType, + TimeSpan, + UserMetricEntry as UserMetricEntry, + UserMetricType as UserMetricType +} from "./Types/Statistics"; +import {toast} from "react-toastify"; export const apiUrl = import.meta.env.VITE_API_URL || "http://localhost:8081"; @@ -182,9 +190,9 @@ export async function createTransaction(receiver: User, value: string, actionTyp } export async function getAllTransactions( - size: number, - page: number, - receiverId: string | undefined + size: number, + page: number, + receiverId: string | undefined ): Promise { const params = receiverId ? "&receiverId=" + receiverId : ""; @@ -418,24 +426,7 @@ export async function uploadItemDisplayPicture(item: ShopItem, file: File): Prom } export async function getItemDisplayPicture(item: ShopItem): Promise { - try { - const result = await fetch(apiUrl + `/api/shop/item/picture?id=${item.id}`, { - method: "GET", - headers: { - Authorization: `Basic ${getEncodedCredentials()}`, - "Content-Type": "application/json", - }, - }); - - if (result.ok && result.status === 200) { - const blob = await result.blob(); - return URL.createObjectURL(blob); - } - } catch (error) { - // If there's a network error or any other error, return null - return undefined; - } - return undefined; + return `${apiUrl}/api/shop/item/picture?id=${item.id}`; } export async function getPersonalInvoices(): Promise { @@ -459,12 +450,12 @@ export async function getPersonalInvoices(): Promise { } export async function getAllInvoices( - page: number, - userId: string | undefined, - mailed: boolean | undefined + page: number, + userId: string | undefined, + mailed: boolean | undefined ): Promise { const params = - (userId ? "&userId=" + userId : "") + (mailed === undefined ? "" : "&mailed=" + (mailed ? "true" : "false")); + (userId ? "&userId=" + userId : "") + (mailed === undefined ? "" : "&mailed=" + (mailed ? "true" : "false")); try { const response = await fetch(`${apiUrl}/api/invoice/list?s=20&p=` + page + params, {