Skip to content

Commit c18989f

Browse files
committed
chore: cleanup source pdf
1 parent 4d1fcfa commit c18989f

File tree

9 files changed

+156
-208
lines changed

9 files changed

+156
-208
lines changed

client/src/pages/ActivityDetails.tsx

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const ActivityDetails: React.FC = () => {
3535

3636
const documentApi = useApi();
3737
const fetchActivity = useCallback(async () => {
38-
if (stateActivity) {
38+
if (stateActivity && !isAdmin) {
3939
return stateActivity;
4040
}
4141

@@ -48,7 +48,7 @@ export const ActivityDetails: React.FC = () => {
4848
}
4949

5050
throw new Error("No activity ID provided");
51-
}, [stateActivity, id]);
51+
}, [stateActivity, id, isAdmin]);
5252

5353
const {
5454
data: activity,
@@ -82,11 +82,9 @@ export const ActivityDetails: React.FC = () => {
8282
window.URL.revokeObjectURL(url);
8383
};
8484

85-
const handleOpenSourcePdf = async () => {
86-
if (!activity?.id || !isAdmin) return;
87-
85+
const handleOpenDocument = async (documentId: string) => {
8886
await documentApi.call(async () => {
89-
await openBlobInNewTab(() => apiService.getActivityPdf(activity.id));
87+
await openBlobInNewTab(() => apiService.downloadDocument(documentId));
9088
});
9189
};
9290

@@ -185,12 +183,9 @@ export const ActivityDetails: React.FC = () => {
185183
(activity.durationMinMinutes || 0) +
186184
(activity.prepTimeMinutes || 0) +
187185
(activity.cleanupTimeMinutes || 0);
188-
189-
const visibleDocuments =
190-
activity.documents?.filter((doc) => isAdmin || doc.type !== "source_pdf") ||
191-
[];
192186
const hasDownloads =
193-
visibleDocuments.length > 0 || (activity.markdowns && activity.markdowns.length > 0);
187+
(activity.documents && activity.documents.length > 0) ||
188+
(activity.markdowns && activity.markdowns.length > 0);
194189

195190
return (
196191
<div className="w-full py-6">
@@ -390,7 +385,7 @@ export const ActivityDetails: React.FC = () => {
390385
</h3>
391386
</div>
392387

393-
{visibleDocuments.map((doc, index) => (
388+
{activity.documents?.map((doc, index) => (
394389
<div
395390
key={doc.id}
396391
className={`flex flex-col gap-4 px-4 py-4 sm:flex-row sm:items-center sm:justify-between${index > 0 ? " border-t border-border" : ""}`}
@@ -407,22 +402,20 @@ export const ActivityDetails: React.FC = () => {
407402
: ""}
408403
</p>
409404
</div>
410-
{doc.type === "source_pdf" && (
411-
<div className="flex items-center gap-2 sm:shrink-0">
412-
<Button
413-
type="button"
414-
variant="outline"
415-
size="sm"
416-
disabled={documentApi.isLoading}
417-
onClick={handleOpenSourcePdf}
418-
title="Download as PDF"
419-
className="flex items-center gap-1.5"
420-
>
421-
<Download className="h-4 w-4 text-red-600" />
422-
<span>PDF</span>
423-
</Button>
424-
</div>
425-
)}
405+
<div className="flex items-center gap-2 sm:shrink-0">
406+
<Button
407+
type="button"
408+
variant="outline"
409+
size="sm"
410+
disabled={documentApi.isLoading}
411+
onClick={() => handleOpenDocument(doc.id)}
412+
title="Open document"
413+
className="flex items-center gap-1.5"
414+
>
415+
<Download className="h-4 w-4 text-red-600" />
416+
<span>PDF</span>
417+
</Button>
418+
</div>
426419
</div>
427420
))}
428421

client/src/services/activityApiService.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,11 @@ export const ActivityApi = {
122122
},
123123

124124
/**
125-
* Get PDF by activity ID
125+
* Download a stored document by document ID
126126
*/
127-
async getActivityPdf(activityId: string) {
127+
async downloadDocument(documentId: string) {
128128
const response = await authService.makeAuthenticatedRequest(
129-
`/api/activities/${activityId}/pdf`,
129+
`/api/documents/${documentId}/download`,
130130
);
131131

132132
if (!response.ok) {

client/src/services/apiService.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export class ApiService {
6767
static deleteActivity = ActivityApi.deleteActivity;
6868
static updateActivity = ActivityApi.updateActivity;
6969
static generateLessonPlan = ActivityApi.generateLessonPlan;
70-
static getActivityPdf = ActivityApi.getActivityPdf;
70+
static downloadDocument = ActivityApi.downloadDocument;
7171
static getMarkdownPdf = ActivityApi.getMarkdownPdf;
7272
static getMarkdownDocx = ActivityApi.getMarkdownDocx;
7373
static getFieldValues = ActivityApi.getFieldValues;
@@ -103,4 +103,3 @@ export class ApiService {
103103

104104
// Export singleton instance for backward compatibility
105105
export const apiService = ApiService;
106-

server/src/main/java/com/learnhub/activitymanagement/controller/ActivityController.java

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import com.learnhub.activitymanagement.dto.request.LessonPlanRequest;
66
import com.learnhub.activitymanagement.dto.request.RecommendationRequest;
77
import com.learnhub.activitymanagement.dto.response.ActivityResponse;
8-
import com.learnhub.activitymanagement.dto.response.DocumentResponse;
98
import com.learnhub.activitymanagement.dto.response.LessonPlanInfoResponse;
109
import com.learnhub.activitymanagement.dto.response.MarkdownResponse;
1110
import com.learnhub.activitymanagement.service.ActivityService;
@@ -33,6 +32,7 @@
3332
import org.springframework.http.MediaType;
3433
import org.springframework.http.ResponseEntity;
3534
import org.springframework.security.access.prepost.PreAuthorize;
35+
import org.springframework.security.core.Authentication;
3636
import org.springframework.web.bind.annotation.*;
3737
import org.springframework.web.multipart.MultipartFile;
3838

@@ -68,15 +68,16 @@ public class ActivityController {
6868
@GetMapping("/")
6969
@PreAuthorize("permitAll()")
7070
@Operation(summary = "Get activities", description = "Get a list of activities with optional filtering and pagination")
71-
public ResponseEntity<?> getActivities(@ModelAttribute ActivityFilterRequest request) {
71+
public ResponseEntity<?> getActivities(@ModelAttribute ActivityFilterRequest request, Authentication authentication) {
7272
logger.info(
7373
"GET /api/activities/ - Get activities called with filters: name={}, ageMin={}, ageMax={}, format={}, limit={}, offset={}",
7474
request.name(), request.ageMin(), request.ageMax(), request.format(), request.limit(),
7575
request.offset());
76+
boolean includeSourcePdf = isAdmin(authentication);
7677
List<ActivityResponse> activities = activityService.getActivitiesWithFilters(request.name(),
7778
request.ageMin(), request.ageMax(), request.durationMin(), request.durationMax(), request.format(),
7879
request.bloomLevel(), request.mentalLoad(), request.physicalEnergy(), request.resourcesNeeded(),
79-
request.topics(), request.limit(), request.offset());
80+
request.topics(), request.limit(), request.offset(), includeSourcePdf);
8081
Map<String, Object> response = new HashMap<>();
8182
response.put("total",
8283
activityService.countActivitiesWithFilters(request.name(), request.ageMin(), request.ageMax(),
@@ -92,9 +93,9 @@ public ResponseEntity<?> getActivities(@ModelAttribute ActivityFilterRequest req
9293
@GetMapping("/{id}")
9394
@PreAuthorize("permitAll()")
9495
@Operation(summary = "Get activity by ID", description = "Get a single activity by its ID")
95-
public ResponseEntity<?> getActivity(@PathVariable UUID id) {
96+
public ResponseEntity<?> getActivity(@PathVariable UUID id, Authentication authentication) {
9697
logger.info("GET /api/activities/{} - Get activity by ID called", id);
97-
ActivityResponse activity = activityService.getActivityById(id);
98+
ActivityResponse activity = activityService.getActivityById(id, isAdmin(authentication));
9899
return ResponseEntity.ok(activity);
99100
}
100101

@@ -135,22 +136,6 @@ public ResponseEntity<?> updateActivity(@PathVariable UUID id, @RequestBody Map<
135136
return ResponseEntity.ok(updated);
136137
}
137138

138-
@GetMapping("/{activityId}/pdf")
139-
@PreAuthorize("permitAll()")
140-
@Operation(summary = "Get activity PDF", description = "Get PDF file for a specific activity")
141-
public ResponseEntity<?> getActivityPdf(@PathVariable UUID activityId) throws IOException {
142-
logger.info("GET /api/activities/{}/pdf - Get activity PDF called", activityId);
143-
ActivityResponse activity = activityService.getActivityById(activityId);
144-
DocumentResponse sourcePdf = activity.getDocuments().stream().filter(d -> "source_pdf".equals(d.getType()))
145-
.findFirst().orElse(null);
146-
if (sourcePdf == null) {
147-
throw new ResourceNotFoundException("PDF not found for this activity");
148-
}
149-
150-
byte[] pdfContent = pdfService.getPdfContent(sourcePdf.getId());
151-
return buildFileDownloadResponse(pdfContent, activity.getName(), ".pdf", MediaType.APPLICATION_PDF, "inline");
152-
}
153-
154139
@GetMapping("/recommendations")
155140
@PreAuthorize("permitAll()")
156141
@Operation(summary = "Get activity recommendations", description = "Get personalized activity recommendations with scoring")
@@ -430,4 +415,9 @@ private String sanitizeDownloadFilename(String name) {
430415
String sanitized = name.replaceAll("[^a-zA-Z0-9._\\- ]", "_").trim();
431416
return sanitized.isEmpty() ? "activity" : sanitized;
432417
}
418+
419+
private boolean isAdmin(Authentication authentication) {
420+
return authentication != null && authentication.getAuthorities().stream()
421+
.anyMatch(authority -> "ROLE_ADMIN".equals(authority.getAuthority()));
422+
}
433423
}

server/src/main/java/com/learnhub/activitymanagement/service/ActivityService.java

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,23 @@ public long countActivitiesWithFilters(String name, Integer ageMin, Integer ageM
5555
public List<ActivityResponse> getActivitiesWithFilters(String name, Integer ageMin, Integer ageMax,
5656
Integer durationMin, Integer durationMax, List<String> formats, List<String> bloomLevels, String mentalLoad,
5757
String physicalEnergy, List<String> resourcesNeeded, List<String> topics, Integer limit, Integer offset) {
58+
return getActivitiesWithFilters(name, ageMin, ageMax, durationMin, durationMax, formats, bloomLevels,
59+
mentalLoad, physicalEnergy, resourcesNeeded, topics, limit, offset, false);
60+
}
61+
62+
public List<ActivityResponse> getActivitiesWithFilters(String name, Integer ageMin, Integer ageMax,
63+
Integer durationMin, Integer durationMax, List<String> formats, List<String> bloomLevels, String mentalLoad,
64+
String physicalEnergy, List<String> resourcesNeeded, List<String> topics, Integer limit, Integer offset,
65+
boolean includeSourcePdf) {
5866
List<Activity> allMatching = getFilteredActivities(name, ageMin, ageMax, durationMin, durationMax, formats,
5967
bloomLevels, mentalLoad, physicalEnergy, resourcesNeeded, topics);
6068

6169
int start = Math.min((offset != null && offset > 0) ? offset : 0, allMatching.size());
6270
int pageSize = (limit != null && limit > 0) ? limit : allMatching.size();
6371
int end = Math.min(start + pageSize, allMatching.size());
6472

65-
return allMatching.subList(start, end).stream().map(this::mapToResponse).collect(Collectors.toList());
73+
return allMatching.subList(start, end).stream().map(activity -> mapToResponse(activity, includeSourcePdf))
74+
.collect(Collectors.toList());
6675
}
6776

6877
private List<Activity> getFilteredActivities(String name, Integer ageMin, Integer ageMax, Integer durationMin,
@@ -147,17 +156,21 @@ private EnergyLevel convertStringToEnergyLevel(String value) {
147156
}
148157

149158
public ActivityResponse getActivityById(UUID id) {
159+
return getActivityById(id, false);
160+
}
161+
162+
public ActivityResponse getActivityById(UUID id, boolean includeSourcePdf) {
150163
logger.debug("Fetching activity by id={}", id);
151164
Activity activity = activityRepository.findById(id)
152165
.orElseThrow(() -> new ResourceNotFoundException("Activity not found"));
153-
return mapToResponse(activity);
166+
return mapToResponse(activity, includeSourcePdf);
154167
}
155168

156169
public ActivityResponse createActivity(Activity activity) {
157170
logger.debug("Saving new activity: name={}", activity.getName());
158171
Activity saved = activityRepository.save(activity);
159172
logger.debug("Activity saved with id={}", saved.getId());
160-
return mapToResponse(saved);
173+
return mapToResponse(saved, false);
161174
}
162175

163176
public ActivityResponse updateActivity(UUID id, Activity activityUpdate) {
@@ -187,7 +200,7 @@ public ActivityResponse updateActivity(UUID id, Activity activityUpdate) {
187200
updateMarkdownByType(activity, activityUpdate, MarkdownType.HINTERGRUNDWISSEN);
188201

189202
Activity saved = activityRepository.save(activity);
190-
return mapToResponse(saved);
203+
return mapToResponse(saved, false);
191204
}
192205

193206
private void updateMarkdownByType(Activity activity, Activity activityUpdate, MarkdownType type) {
@@ -328,10 +341,14 @@ public Activity createActivityFromMap(Map<String, Object> data) {
328341
}
329342

330343
public ActivityResponse convertToResponse(Activity activity) {
331-
return mapToResponse(activity);
344+
return convertToResponse(activity, false);
345+
}
346+
347+
public ActivityResponse convertToResponse(Activity activity, boolean includeSourcePdf) {
348+
return mapToResponse(activity, includeSourcePdf);
332349
}
333350

334-
private ActivityResponse mapToResponse(Activity activity) {
351+
private ActivityResponse mapToResponse(Activity activity, boolean includeSourcePdf) {
335352
ActivityResponse response = new ActivityResponse();
336353
response.setId(activity.getId());
337354
response.setName(activity.getName());
@@ -351,9 +368,10 @@ private ActivityResponse mapToResponse(Activity activity) {
351368
response.setResourcesNeeded(activity.getResourcesNeeded());
352369
response.setTopics(activity.getTopics());
353370

354-
// Map all documents to response list
355-
List<DocumentResponse> docResponses = activity.getDocuments().stream().map(d -> new DocumentResponse(d.getId(),
356-
d.getFilename(), d.getFileSize(), d.getType() != null ? d.getType().getValue() : null))
371+
List<DocumentResponse> docResponses = activity.getDocuments().stream()
372+
.filter(document -> includeSourcePdf || document.getType() != DocumentType.SOURCE_PDF)
373+
.map(d -> new DocumentResponse(d.getId(), d.getFilename(), d.getFileSize(),
374+
d.getType() != null ? d.getType().getValue() : null))
357375
.collect(Collectors.toList());
358376
response.setDocuments(docResponses);
359377

server/src/main/java/com/learnhub/documentmanagement/controller/DocumentsController.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
package com.learnhub.documentmanagement.controller;
22

3+
import com.learnhub.activitymanagement.entity.enums.DocumentType;
34
import com.learnhub.documentmanagement.entity.PDFDocument;
45
import com.learnhub.documentmanagement.service.PDFService;
56
import com.learnhub.dto.response.ErrorResponse;
67
import io.swagger.v3.oas.annotations.Operation;
8+
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
79
import io.swagger.v3.oas.annotations.tags.Tag;
10+
import java.io.IOException;
811
import java.util.HashMap;
912
import java.util.Map;
1013
import java.util.UUID;
1114
import org.slf4j.Logger;
1215
import org.slf4j.LoggerFactory;
1316
import org.springframework.beans.factory.annotation.Autowired;
17+
import org.springframework.http.HttpHeaders;
18+
import org.springframework.http.MediaType;
1419
import org.springframework.http.ResponseEntity;
20+
import org.springframework.security.access.AccessDeniedException;
1521
import org.springframework.security.access.prepost.PreAuthorize;
22+
import org.springframework.security.core.Authentication;
1623
import org.springframework.web.bind.annotation.*;
1724

1825
@RestController
@@ -47,4 +54,52 @@ public ResponseEntity<?> getDocumentInfo(@PathVariable UUID documentId) {
4754
return ResponseEntity.status(404).body(ErrorResponse.of("Document not found: " + e.getMessage()));
4855
}
4956
}
57+
58+
@GetMapping("/{documentId}/download")
59+
@PreAuthorize("permitAll()")
60+
@Operation(summary = "Download document", description = "Download a stored document. SOURCE_PDF documents require admin rights.")
61+
@SecurityRequirement(name = "BearerAuth")
62+
public ResponseEntity<?> downloadDocument(@PathVariable UUID documentId, Authentication authentication)
63+
throws IOException {
64+
logger.info("GET /api/documents/{}/download - Download document called", documentId);
65+
try {
66+
PDFDocument document = pdfService.getPdfDocument(documentId);
67+
enforceDownloadAccess(document, authentication);
68+
69+
byte[] pdfContent = pdfService.getPdfContent(documentId);
70+
String filename = sanitizeFilename(document.getFilename());
71+
72+
HttpHeaders headers = new HttpHeaders();
73+
headers.setContentType(MediaType.APPLICATION_PDF);
74+
headers.setContentDispositionFormData("attachment", filename);
75+
headers.setContentLength(pdfContent.length);
76+
77+
return ResponseEntity.ok().headers(headers).body(pdfContent);
78+
} catch (AccessDeniedException e) {
79+
throw e;
80+
} catch (Exception e) {
81+
logger.error("GET /api/documents/{}/download - Document not found: {}", documentId, e.getMessage());
82+
return ResponseEntity.status(404).body(ErrorResponse.of("Document not found: " + e.getMessage()));
83+
}
84+
}
85+
86+
private void enforceDownloadAccess(PDFDocument document, Authentication authentication) {
87+
if (document.getType() != DocumentType.SOURCE_PDF) {
88+
return;
89+
}
90+
91+
boolean isAdmin = authentication != null && authentication.getAuthorities().stream()
92+
.anyMatch(authority -> "ROLE_ADMIN".equals(authority.getAuthority()));
93+
if (!isAdmin) {
94+
throw new AccessDeniedException("Admin rights required to download source PDFs");
95+
}
96+
}
97+
98+
private String sanitizeFilename(String name) {
99+
if (name == null || name.isBlank()) {
100+
return "document.pdf";
101+
}
102+
String sanitized = name.replaceAll("[^a-zA-Z0-9._\\- ]", "_").trim();
103+
return sanitized.isEmpty() ? "document.pdf" : sanitized;
104+
}
50105
}

0 commit comments

Comments
 (0)