Let project members download a project as JSON
Java · Spring Boot · intermediate · modification
Adds a JSON export of a project for the upcoming Export button. `GET /api/project-exports/{projectId}` returns the project's name and all of its tasks in id order as a download named `project-<id>.json`. `ProjectExportService` loads the project (404 when it does not exist) and its tasks in one read-only transaction, and `TaskRepository` gains the ordered finder. `WebConfig` and `ProjectMembershipInterceptor` are shown for reference and are unchanged.
Task-tracker backend on Spring Boot 3.3 (Java 21), Spring MVC with the default `PathPatternParser`, Spring Data JPA (Hibernate 6) on PostgreSQL, Spring Security with session login. Every `/api/**` request needs a signed-in session (anonymous requests get 401 before reaching MVC); the principal is a `TaskUser(Long userId, String email)` record and `userId` is never null. Project-scoped endpoints rely on `ProjectMembershipInterceptor` as registered in `WebConfig` (both shown, unchanged); controllers do not repeat the membership check. Existing and unchanged: the `Project` (`id`, `name`), `Task` (`id`, `projectId`, `title`, `status` as a `TaskStatus` enum stored by name, nullable `dueDate`) and `Membership` (`projectId`, `userId`) entities, `ProjectRepository extends JpaRepository<Project, Long>`, and `MembershipRepository.existsByProjectIdAndUserId`. Project ids are sequential `bigserial` values. Jackson writes `LocalDate` as `yyyy-MM-dd` (Spring Boot's default).
Requirements
- A member of a project can download that project as a JSON file: the project's `id` and `name`, and every task of the project (`id`, `title`, `status`, `dueDate` or null) ordered by task id, sent with `Content-Disposition: attachment; filename="project-<id>.json"`. The web app's Export button comes in a separate frontend PR and will call whatever path this PR exposes.
- Only members of the project may export it. A signed-in user who is not a member of the project, and a request for a project id that does not exist, both get 404, as on every other project endpoint, so the response does not reveal whether a project exists.
- Sign-in is already required for every `/api/**` request by the existing security configuration. A project has at most 5,000 tasks, so the export is built in memory.
Files touched
- src/main/java/com/acme/tasks/exports/ProjectExportController.java
- src/main/java/com/acme/tasks/exports/ProjectExportService.java
- src/main/java/com/acme/tasks/exports/ProjectExport.java
- src/main/java/com/acme/tasks/projects/TaskRepository.java
- src/main/java/com/acme/tasks/web/WebConfig.java
- src/main/java/com/acme/tasks/web/ProjectMembershipInterceptor.java
--- src/main/java/com/acme/tasks/exports/ProjectExportController.java
+package com.acme.tasks.exports;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class ProjectExportController {
+
+ private final ProjectExportService exports;
+
+ public ProjectExportController(ProjectExportService exports) {
+ this.exports = exports;
+ }
+
+ @GetMapping("/api/project-exports/{projectId}")
+ public ResponseEntity<ProjectExport> export(@PathVariable Long projectId) {
+ ProjectExport body = exports.export(projectId);
+ return ResponseEntity.ok()
+ .header(HttpHeaders.CONTENT_DISPOSITION,
+ "attachment; filename=\"project-" + body.id() + ".json\"")
+ .body(body);
+ }
+}
+
--- src/main/java/com/acme/tasks/exports/ProjectExportService.java
+package com.acme.tasks.exports;