Skip to content

Delete Image ownership check - #1450

Open
RevTpark wants to merge 5 commits into
mainfrom
fix/delete-image
Open

Delete Image ownership check#1450
RevTpark wants to merge 5 commits into
mainfrom
fix/delete-image

Conversation

@RevTpark

@RevTpark RevTpark commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

  • Adds a proper ownership to images metadata for grants and profile/sponsor uploads.
  • delete-image endpoint is no longer unrestricted and has validations in place to only allow auth users to delete their own images.
  • does not break any legacy image uploads, fallback added wherever necessary.
  • consolidate the image source types to be a single source of truth.
  • fixed a bug where profile/sponsor image would not delete previously uploaded images if save/update was not performed in the session.

Where should the reviewer start?

How should this be manually tested?

Any background context you want to provide?

What are the relevant issues?

Screenshots (if appropriate)

Summary by CodeRabbit

  • Bug Fixes

    • Improved authorization for deleting grant, profile, and sponsor images.
    • Unauthorized and unsupported deletion requests are now safely denied.
    • Improved removal of image and raw media uploads.
    • Removed redundant validation and legacy handling for missing images.
  • Enhancements

    • Uploads now retain ownership metadata for more reliable authorization.
    • Grant-related uploads and signing include ownership information where applicable.

@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
earn Ready Ready Preview Aug 4, 2026 10:27am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Image uploads now include owner metadata. The deletion route validates source folders and ownership for profile, sponsor, grant, and configured image sources. Cloudinary checks both image and raw resources during ownership lookup and deletion.

Changes

Image ownership authorization

Layer / File(s) Summary
Attach owner context to uploads
src/lib/image-upload/types.ts, src/lib/image-upload/cloudinary-client.ts, src/app/api/image/sign/route.ts, src/hooks/use-image-upload.ts, src/components/tiptap/hooks/use-minimal-tiptap.ts
Signed upload parameters carry the authenticated owner ID as Cloudinary context. Both upload paths submit the returned context.
Read ownership and delete resources
src/lib/image-upload/cloudinary-client.ts, src/lib/image-upload/index.ts
Cloudinary helpers read owner_id from image or raw resources. Deletion attempts both resource types.
Authorize source-specific deletion
src/app/api/image/delete/route.ts
The route validates configured folders, checks persisted or metadata ownership, supports legacy grant tranche URLs, and denies unauthorized or unsupported sources.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SignRoute
  participant generateSignedUploadParams
  participant UploadHook
  participant TiptapUpload
  participant Cloudinary
  SignRoute->>generateSignedUploadParams: Pass source and authenticated userId
  generateSignedUploadParams-->>SignRoute: Return signed parameters with owner_id context
  SignRoute-->>UploadHook: Return upload parameters
  UploadHook->>Cloudinary: Upload with context
  TiptapUpload->>Cloudinary: Upload with context
Loading
sequenceDiagram
  participant Client
  participant DeleteRoute
  participant Cloudinary
  participant GrantTranches
  Client->>DeleteRoute: Request image deletion
  DeleteRoute->>DeleteRoute: Validate source folder and authorization
  DeleteRoute->>Cloudinary: Read owner metadata
  Cloudinary-->>DeleteRoute: Return owner_id or not found
  DeleteRoute->>GrantTranches: Check legacy grant ownership when needed
  GrantTranches-->>DeleteRoute: Return ownership result
  DeleteRoute->>Cloudinary: Delete image and raw resources when authorized
  Cloudinary-->>DeleteRoute: Return deletion result
Loading

Poem

A rabbit marks each upload bright,
With owner context locked tight.
Folder checks guard every file,
Image and raw paths face the trial.
Unauthorized hops away,
Safe images stay another day.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding ownership checks for image deletion.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/delete-image

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/lib/image-upload/types.ts (1)

26-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use explicit | undefined and readonly for the new field.

The new context?: string; field uses the optional-property shorthand. Use context: string | undefined; instead, so object literals must explicitly assign the field. Also mark it readonly, since SignedUploadParams values are not mutated after construction.

As per coding guidelines, "Use property: Type | undefined instead of property?: Type for TypeScript type definitions to force explicit property passing and prevent bugs from accidentally omitting required properties" and "Use readonly properties for object types by default in TypeScript to prevent accidental mutation at runtime."

♻️ Proposed fix
 export interface SignedUploadParams {
-  signature: string;
-  timestamp: number;
-  cloudName: string;
-  apiKey: string;
-  folder: string;
-  publicId?: string;
-  context?: string;
-  eager?: string;
+  readonly signature: string;
+  readonly timestamp: number;
+  readonly cloudName: string;
+  readonly apiKey: string;
+  readonly folder: string;
+  readonly publicId: string | undefined;
+  readonly context: string | undefined;
+  readonly eager: string | undefined;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/image-upload/types.ts` at line 26, Update the new context field in
SignedUploadParams to be readonly and explicitly typed as string | undefined
instead of using optional-property shorthand, ensuring object literals must
provide it while preserving its string-or-undefined value.

Source: Coding guidelines

src/lib/image-upload/cloudinary-client.ts (1)

57-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type the caught error as unknown, not any.

catch (error: any) disables type checking for error. The rest of the codebase (for example sign/route.ts and delete/route.ts) types caught errors as unknown. Use unknown here too, and narrow before reading http_code.

As per coding guidelines, "Outside of generic functions, use any type extremely sparingly."

♻️ Proposed fix
-    } catch (error: any) {
-      if (error?.http_code !== 404 && error?.error?.http_code !== 404) {
-        throw error;
-      }
-    }
+    } catch (error: unknown) {
+      const httpCode =
+        (error as { http_code?: number })?.http_code ??
+        (error as { error?: { http_code?: number } })?.error?.http_code;
+      if (httpCode !== 404) {
+        throw error;
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/image-upload/cloudinary-client.ts` around lines 57 - 75, Update the
catch clause in getImageOwnerId to type error as unknown, then narrow it before
accessing http_code or error.http_code while preserving the existing 404
handling and rethrow behavior for other errors.

Source: Coding guidelines

src/app/api/image/delete/route.ts (1)

47-75: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Consider filtering grant tranches at the database level.

isLegacyGrantImageOwnedByUser loads every grantTranche row for the user, then scans each JSON array in application code to find the matching public ID. For users with many tranches or large eventPictures/eventReceipts/aiReceipts arrays, this pulls more data into memory than necessary. A database-level JSON containment filter would scale better, though this is not urgent given the current expected data volumes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/image/delete/route.ts` around lines 47 - 75, Update
isLegacyGrantImageOwnedByUser to filter grantTranche records in the Prisma query
using the selected source field and publicId JSON containment, returning whether
any matching row exists via the database instead of loading all arrays and
scanning them with jsonContainsPublicId.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/api/image/delete/route.ts`:
- Around line 33-36: Extract the grant image source list into a shared exported
as-const array and derive GrantImageSource from it. In
src/app/api/image/delete/route.ts lines 33-36, 262-266, and 65-69, import and
use the shared symbols for type narrowing, source checks, and fieldBySource
keys; in src/app/api/image/sign/route.ts lines 103-112, replace the local
grantImageSources Set with the shared list. Ensure all four sites use one source
of truth.

In `@src/lib/image-upload/cloudinary-client.ts`:
- Around line 77-87: Update deleteImage so its catch block does not silently
discard failures from cloudinary.uploader.destroy: distinguish expected
not-found responses from other errors using the existing getImageOwnerId
pattern, or at minimum log caught errors with relevant context before
continuing. Preserve the retry across both resource types and the existing
boolean result behavior.

---

Nitpick comments:
In `@src/app/api/image/delete/route.ts`:
- Around line 47-75: Update isLegacyGrantImageOwnedByUser to filter grantTranche
records in the Prisma query using the selected source field and publicId JSON
containment, returning whether any matching row exists via the database instead
of loading all arrays and scanning them with jsonContainsPublicId.

In `@src/lib/image-upload/cloudinary-client.ts`:
- Around line 57-75: Update the catch clause in getImageOwnerId to type error as
unknown, then narrow it before accessing http_code or error.http_code while
preserving the existing 404 handling and rethrow behavior for other errors.

In `@src/lib/image-upload/types.ts`:
- Line 26: Update the new context field in SignedUploadParams to be readonly and
explicitly typed as string | undefined instead of using optional-property
shorthand, ensuring object literals must provide it while preserving its
string-or-undefined value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ab15bbd5-1f22-4e33-b120-84e104441d95

📥 Commits

Reviewing files that changed from the base of the PR and between cb8c5b7 and d99be6e.

📒 Files selected for processing (6)
  • src/app/api/image/delete/route.ts
  • src/app/api/image/sign/route.ts
  • src/hooks/use-image-upload.ts
  • src/lib/image-upload/cloudinary-client.ts
  • src/lib/image-upload/index.ts
  • src/lib/image-upload/types.ts

Comment thread src/app/api/image/delete/route.ts Outdated
Comment on lines +33 to +36
type GrantImageSource =
| 'grant-event-pictures'
| 'grant-event-receipts'
| 'grant-agentic-receipts';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Extract a single shared list of grant image sources.

The set of "grant image" sources (grant-event-pictures, grant-event-receipts, grant-agentic-receipts) is defined independently in four places across two files. This is one root cause: no shared source of truth. A future addition or rename of a grant source requires updating all four sites; missing one silently breaks either context attachment on upload or ownership verification on delete.

  • src/app/api/image/delete/route.ts#L33-L36: derive GrantImageSource from a single exported as const array (e.g. in config.ts), instead of an independent literal union.
  • src/app/api/image/delete/route.ts#L262-L266: replace the else if literal comparisons with a check against the shared array/set (e.g. GRANT_IMAGE_SOURCES.includes(source)), narrowing source to GrantImageSource.
  • src/app/api/image/delete/route.ts#L65-L69: keep fieldBySource keyed by the shared GrantImageSource type so TypeScript flags a missing entry when a new grant source is added.
  • src/app/api/image/sign/route.ts#L103-L112: replace the inline grantImageSources Set with the same shared array/set, instead of a locally re-declared list.
♻️ Proposed shared constant
// src/lib/image-upload/config.ts
export const GRANT_IMAGE_SOURCES = [
  'grant-event-pictures',
  'grant-event-receipts',
  'grant-agentic-receipts',
] as const;
export type GrantImageSource = (typeof GRANT_IMAGE_SOURCES)[number];
// src/app/api/image/sign/route.ts
-    const grantImageSources = new Set([
-      'grant-event-pictures',
-      'grant-event-receipts',
-      'grant-agentic-receipts',
-    ]);
+    const grantImageSources = new Set(GRANT_IMAGE_SOURCES);
// src/app/api/image/delete/route.ts
-type GrantImageSource =
-  | 'grant-event-pictures'
-  | 'grant-event-receipts'
-  | 'grant-agentic-receipts';
+import type { GrantImageSource } from '`@/lib/image-upload/config`';
+import { GRANT_IMAGE_SOURCES } from '`@/lib/image-upload/config`';
...
-    } else if (
-      source === 'grant-event-pictures' ||
-      source === 'grant-event-receipts' ||
-      source === 'grant-agentic-receipts'
-    ) {
+    } else if (
+      (GRANT_IMAGE_SOURCES as readonly string[]).includes(source)
+    ) {
📍 Affects 2 files
  • src/app/api/image/delete/route.ts#L33-L36 (this comment)
  • src/app/api/image/delete/route.ts#L262-L266
  • src/app/api/image/delete/route.ts#L65-L69
  • src/app/api/image/sign/route.ts#L103-L112
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/image/delete/route.ts` around lines 33 - 36, Extract the grant
image source list into a shared exported as-const array and derive
GrantImageSource from it. In src/app/api/image/delete/route.ts lines 33-36,
262-266, and 65-69, import and use the shared symbols for type narrowing, source
checks, and fieldBySource keys; in src/app/api/image/sign/route.ts lines
103-112, replace the local grantImageSources Set with the shared list. Ensure
all four sites use one source of truth.

Comment thread src/lib/image-upload/cloudinary-client.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant