From f7bbe247d755e1be25e0e9f23122559d1b3c9465 Mon Sep 17 00:00:00 2001 From: Venkata Nainala Date: Wed, 18 Mar 2026 09:17:05 +0000 Subject: [PATCH 1/6] fix: filter default draft lookup by team_id to prevent cross-team mismatch findDefaultDraft only filtered by owner_id, which could return a draft from a different team. When the user uploaded files to that draft and refreshed at step 2, UserDrafts::execute() filtered by the current team_id and the draft would not appear in results. --- app/Actions/Draft/UserDrafts.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Actions/Draft/UserDrafts.php b/app/Actions/Draft/UserDrafts.php index c3f7246da..6bbc5ddb4 100644 --- a/app/Actions/Draft/UserDrafts.php +++ b/app/Actions/Draft/UserDrafts.php @@ -28,7 +28,7 @@ public function execute(User $user): Collection } /** - * Find existing default draft without files. + * Find existing default draft without files for the user's current team. */ public function findDefaultDraft(User $user): ?Draft { @@ -36,6 +36,7 @@ public function findDefaultDraft(User $user): ?Draft return Draft::doesntHave('files') ->where('owner_id', $user_id) + ->where('team_id', $team_id) ->first(); } From 652b93fd724d7f3c1052d5f7ea6aa5b97ee93f73 Mon Sep 17 00:00:00 2001 From: Venkata Nainala Date: Wed, 18 Mar 2026 09:17:10 +0000 Subject: [PATCH 2/6] feat: add draft show endpoint for direct lookup by ID Add DraftController::show() with ownership verification so the frontend can fetch a specific draft when it is not found in the pre-loaded team- scoped list. This provides a fallback for drafts that were created under a different team context. --- app/Http/Controllers/DraftController.php | 18 ++++++++++++++++++ routes/web.php | 2 ++ 2 files changed, 20 insertions(+) diff --git a/app/Http/Controllers/DraftController.php b/app/Http/Controllers/DraftController.php index eb9e2fc8a..603ec3c0c 100644 --- a/app/Http/Controllers/DraftController.php +++ b/app/Http/Controllers/DraftController.php @@ -81,6 +81,24 @@ public function missingFiles(Request $request, Draft $draft): JsonResponse return response()->json($missingFilesData); } + /** + * Get a single draft by ID with ownership verification. + */ + public function show(Request $request, Draft $draft): JsonResponse + { + /** @var \App\Models\User $user */ + $user = Auth::user(); + [$user_id] = $user->getUserTeamData(); + + if ($draft->owner_id !== $user_id) { + abort(403); + } + + return response()->json([ + 'draft' => $draft->load('Tags'), + ]); + } + /** * Update draft properties. */ diff --git a/routes/web.php b/routes/web.php index 66cb84a7a..26a765e45 100644 --- a/routes/web.php +++ b/routes/web.php @@ -295,6 +295,8 @@ Route::post('datasets/{dataset}/snapshot', [DatasetController::class, 'snapshot']) ->name('dashboard.dataset.snapshot'); + Route::get('drafts/{draft}/show', [DraftController::class, 'show']) + ->name('dashboard.draft.show'); Route::get('drafts/{draft}/info', [DraftController::class, 'info']) ->name('dashboard.draft.info'); Route::get('drafts/{draft}/files', [DraftController::class, 'files']) From 1a6e6855f49a47e3dd991e856f58a19b935e6f54 Mon Sep 17 00:00:00 2001 From: Venkata Nainala Date: Wed, 18 Mar 2026 09:17:16 +0000 Subject: [PATCH 3/6] fix: persist draft_id in URL at step 2 and add fallback draft lookup selectStep(2) did not set draft_id in the query string, relying on it persisting from step 1. Now it explicitly sets it. When a page refresh at step 2 cannot find the draft in the team-scoped list, the frontend falls back to a direct fetch via the new show endpoint. --- resources/js/Pages/Upload.vue | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/resources/js/Pages/Upload.vue b/resources/js/Pages/Upload.vue index 2c70ea5b9..53db55a1f 100644 --- a/resources/js/Pages/Upload.vue +++ b/resources/js/Pages/Upload.vue @@ -2581,14 +2581,23 @@ export default { (d) => d.id == this.draft_id ); } + if (!selectedDraft && response.data.default.id == this.draft_id) { + selectedDraft = response.data.default; + } if (selectedDraft) { this.selectDraft(selectedDraft); + this.loading = false; } else { - if (response.data.default.id == this.draft_id) { - this.selectDraft(response.data.default); - } + this.fetchDraftById(this.draft_id).then( + (draftResponse) => { + this.selectDraft(draftResponse.data.draft); + this.loading = false; + }, + () => { + this.loading = false; + } + ); } - this.loading = false; } else { alert( "Could not find the draft. Redirecting to the upload page." @@ -2643,6 +2652,9 @@ export default { this.loading = true; return axios.get("/dashboard/drafts"); }, + fetchDraftById(draftId) { + return axios.get("/dashboard/drafts/" + draftId + "/show"); + }, formatStatus(status) { if (!status) return ""; @@ -2800,9 +2812,10 @@ export default { } else if (id == 2) { this.updateDraft(null, 2); this.$nextTick(function () { - // if (this.$refs.spectraEditorREF) { - // this.$refs.spectraEditorREF.registerEvents(); - // } + this.setQueryStringParameter( + "draft_id", + this.currentDraft.id + ); this.setQueryStringParameter("step", 2); this.step = "2"; this.fetchValidations(); From d5ee43a1ef31cfd72682bd24e636e1b0e0433a88 Mon Sep 17 00:00:00 2001 From: Venkata Nainala Date: Thu, 19 Mar 2026 10:04:27 +0000 Subject: [PATCH 4/6] fix: npm updates --- package-lock.json | 9 +++++---- yarn.lock | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3248603c3..938484286 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,10 @@ { - "name": "nmrxiv", + "name": "html", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "html", "dependencies": { "@headlessui/vue": "^1.6.2", "@heroicons/vue": "^2.0.11", @@ -3566,9 +3567,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, diff --git a/yarn.lock b/yarn.lock index b2c492a2d..4f37aeaca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2085,9 +2085,9 @@ flat-cache@^3.0.4: rimraf "^3.0.2" flatted@^3.2.9: - version "3.3.3" - resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz" - integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== + version "3.4.2" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz" + integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== focus-trap@^7, focus-trap@^7.6.4: version "7.8.0" From f49ab75d140d72752e54c0e488131f6b5daf3458 Mon Sep 17 00:00:00 2001 From: Venkata Nainala Date: Thu, 19 Mar 2026 10:08:26 +0000 Subject: [PATCH 5/6] fix: js formatting --- resources/js/Pages/Upload.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/resources/js/Pages/Upload.vue b/resources/js/Pages/Upload.vue index 53db55a1f..1000b5a9a 100644 --- a/resources/js/Pages/Upload.vue +++ b/resources/js/Pages/Upload.vue @@ -2581,7 +2581,10 @@ export default { (d) => d.id == this.draft_id ); } - if (!selectedDraft && response.data.default.id == this.draft_id) { + if ( + !selectedDraft && + response.data.default.id == this.draft_id + ) { selectedDraft = response.data.default; } if (selectedDraft) { From 6c088e111b3085bda5c835e7804162fae5163039 Mon Sep 17 00:00:00 2001 From: Venkata Nainala Date: Thu, 19 Mar 2026 10:37:29 +0000 Subject: [PATCH 6/6] fix: coverage updates --- tests/Feature/DraftFeatureTest.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/Feature/DraftFeatureTest.php b/tests/Feature/DraftFeatureTest.php index b02f0ae8e..0a1942346 100644 --- a/tests/Feature/DraftFeatureTest.php +++ b/tests/Feature/DraftFeatureTest.php @@ -285,6 +285,35 @@ public function test_nonexistent_draft_returns_404(): void $response->assertStatus(404); } + public function test_owner_can_show_draft(): void + { + $response = $this->actingAs($this->user) + ->get("/dashboard/drafts/{$this->draft->id}/show"); + + $response->assertStatus(200); + + $responseData = $response->json(); + $this->assertArrayHasKey('draft', $responseData); + $this->assertEquals($this->draft->id, $responseData['draft']['id']); + } + + public function test_show_draft_returns_403_for_non_owner(): void + { + $otherUser = User::factory()->withPersonalTeam()->create(); + + $response = $this->actingAs($otherUser) + ->get("/dashboard/drafts/{$this->draft->id}/show"); + + $response->assertStatus(403); + } + + public function test_show_draft_requires_authentication(): void + { + $response = $this->get("/dashboard/drafts/{$this->draft->id}/show"); + + $response->assertStatus(302); + } + public function test_draft_processed_mail_can_be_rendered(): void { $project = Project::factory()->create([