Skip to content

SUMMA-34: Image Storage in Development and AWS S3#127

Merged
BengeeL merged 8 commits into
devfrom
SUMMA-34
Jun 1, 2025
Merged

SUMMA-34: Image Storage in Development and AWS S3#127
BengeeL merged 8 commits into
devfrom
SUMMA-34

Conversation

@BengeeL
Copy link
Copy Markdown
Member

@BengeeL BengeeL commented May 30, 2025

Summary by CodeRabbit

  • New Features

    • Added support for local file uploads in development and AWS S3 storage in production for content and profile images.
    • Enabled direct access to uploaded files via a new static file route.
  • Improvements

    • Unified content creation and editing into a single editor with explicit mode handling and improved error messaging.
    • Enhanced error handling and input validation for file uploads and content editing.
    • Updated routing for clearer separation between content creation and editing.
    • Improved security headers and content security policy.
    • Updated Stripe API version and Express type definitions.
    • Refined user authentication handling and navigation in content views.
  • Bug Fixes

    • Corrected navigation paths in content editing workflows.
  • Chores

    • Updated dependencies and added new AWS SDK packages.
    • Improved code consistency and formatting.
    • Updated .gitignore to exclude local uploads.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 30, 2025

Warning

Rate limit exceeded

@BengeeL has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 23 minutes and 22 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 092510d and da0220e.

📒 Files selected for processing (2)
  • backend/src/modules/storage/services/storage.service.ts (1 hunks)
  • frontend/src/pages/content/ContentEditor.tsx (5 hunks)

Walkthrough

The changes introduce local file upload support for development and switch production file storage from Firebase to AWS S3, with corresponding updates to backend services and controllers. The frontend's content editor is unified for both creation and editing, removing AI summarization, and routing is updated accordingly. Additional validation and error handling improvements are included throughout.

Changes

Files/Paths Change Summary
.gitignore Added local_uploads to ignored files/directories.
backend/package.json Updated Express types; added AWS S3 SDK packages.
backend/src/app.config.ts, backend/src/app.ts Standardized string quotes; updated helmet config with CSP and CORP; added static serving for local_uploads.
backend/src/modules/content/controllers/content.controller.ts, Improved file input validation and error handling in upload/edit endpoints.
backend/src/modules/user/controllers/user.controller.ts Added validation for uploaded profile image presence.
backend/src/modules/storage/services/storage.service.ts Refactored: replaced Firebase with AWS S3 for production, added local filesystem storage for development, updated upload/delete interfaces.
backend/src/modules/subscription/services/stripe.service.ts Updated Stripe API version.
frontend/src/App.tsx Split content editor route into create/edit; passed isEditMode prop.
frontend/src/pages/content/ContentEditor.tsx Unified create/edit modes; removed AI summarization; improved error handling and API logic.
frontend/src/pages/content/ContentView.tsx Refactored user reference from destructured user to auth.user; fixed navigation path; improved creator check.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend (ContentEditor)
    participant Backend (ContentController)
    participant StorageService
    participant AWS S3/Local FS

    User->>Frontend (ContentEditor): Submit content (create/edit) with file
    Frontend (ContentEditor)->>Backend (ContentController): POST/PUT content, multipart with file
    Backend (ContentController)->>StorageService: uploadFile(file, path, name, type)
    alt Development
        StorageService->>Local FS: Write file to local_uploads/
        Local FS-->>StorageService: Return local URL
    else Production
        StorageService->>AWS S3: PutObjectCommand (upload file)
        AWS S3-->>StorageService: Confirm upload
        StorageService->>AWS S3: Generate signed URL
        AWS S3-->>StorageService: Return signed URL
    end
    StorageService-->>Backend (ContentController): Return file URL
    Backend (ContentController)-->>Frontend (ContentEditor): Respond with content info
    Frontend (ContentEditor)-->>User: Redirect or show result
Loading

Suggested reviewers

  • Mindful-Developer
  • Hi-kue

Poem

A rabbit hopped by, with files to store,
Now S3 and local keep uploads galore!
The editor’s smarter, no AI in sight,
Create or edit—both modes feel right.
With careful checks and routes anew,
This code is fresh as morning dew!
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (5)
.gitignore (1)

191-191: LGTM! Good addition for local development file storage.

Adding local_uploads to gitignore is appropriate to prevent tracking of locally uploaded files during development. This aligns well with the local file storage implementation.

Consider grouping this with other directory exclusions for better organization:

# Dependency directories
node_modules/
jspm_packages/
+local_uploads
backend/src/modules/user/controllers/user.controller.ts (1)

141-143: Good validation addition, but consider enhancing the file check.

Adding file validation before upload is excellent defensive programming. However, the current check could be more robust.

Consider enhancing the validation to handle edge cases:

-  if (!file) {
-    return res.status(400).json({ error: "No file uploaded." });
-  }
+  if (!files.profileImage || !files.profileImage[0]) {
+    return res.status(400).json({ error: "No file uploaded." });
+  }

This prevents potential errors if files.profileImage is undefined or empty.

backend/src/modules/content/controllers/content.controller.ts (1)

451-451: Verify query parameter extraction precedence.

The parentheses around the type assertion ensure correct operator precedence, but consider using a more explicit approach for better readability.

Consider this more explicit approach for better clarity:

-    const userId = (req.query.userId as string) || undefined;
+    const userId = typeof req.query.userId === 'string' ? req.query.userId : undefined;
frontend/src/pages/content/ContentEditor.tsx (1)

199-201: Move authentication check to useEffect for better practice.

While the current placement works, it's better to handle navigation side effects in useEffect.

-  // User must be authenticated to create content
-  if (!auth.isAuthenticated) {
-    navigate("/authentication/login");
-  }
+  // User must be authenticated to create content
+  useEffect(() => {
+    if (!auth.isAuthenticated) {
+      navigate("/authentication/login");
+    }
+  }, [auth.isAuthenticated, navigate]);
backend/src/modules/storage/services/storage.service.ts (1)

240-249: Consider handling different S3 URL formats.

S3 supports both path-style and virtual-hosted-style URLs. The current implementation might not handle all cases correctly.

Enhanced implementation to handle both URL styles:

   static extractFilePathFromUrl(url: string): string {
     try {
       const u = new URL(url);
-      // Remove leading slash from pathname
-      return u.pathname.replace(/^\/+/, "");
+      
+      // Handle virtual-hosted-style: https://bucket.s3.region.amazonaws.com/key
+      if (u.hostname.includes('.s3.') && u.hostname.includes('.amazonaws.com')) {
+        return u.pathname.replace(/^\/+/, "");
+      }
+      
+      // Handle path-style: https://s3.region.amazonaws.com/bucket/key
+      if (u.hostname.startsWith('s3.') && u.hostname.includes('.amazonaws.com')) {
+        const pathParts = u.pathname.split('/').filter(Boolean);
+        pathParts.shift(); // Remove bucket name
+        return pathParts.join('/');
+      }
+      
+      // Default case
+      return u.pathname.replace(/^\/+/, "");
     } catch {
-      throw new Error("Invalid S3 URL");
+      throw new Error(`Invalid S3 URL: ${url}`);
     }
   }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d47fe85 and 19ccb57.

📒 Files selected for processing (11)
  • .gitignore (1 hunks)
  • backend/package.json (2 hunks)
  • backend/src/app.config.ts (1 hunks)
  • backend/src/app.ts (2 hunks)
  • backend/src/modules/content/controllers/content.controller.ts (4 hunks)
  • backend/src/modules/storage/services/storage.service.ts (1 hunks)
  • backend/src/modules/subscription/services/stripe.service.ts (1 hunks)
  • backend/src/modules/user/controllers/user.controller.ts (1 hunks)
  • frontend/src/App.tsx (1 hunks)
  • frontend/src/pages/content/ContentEditor.tsx (5 hunks)
  • frontend/src/pages/content/ContentView.tsx (11 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (7)
backend/src/app.ts (1)
backend/src/shared/utils/response.ts (1)
  • createErrorResponse (26-36)
backend/src/modules/user/controllers/user.controller.ts (1)
backend/src/modules/storage/services/storage.service.ts (1)
  • StorageService (49-250)
frontend/src/App.tsx (1)
frontend/src/pages/content/ContentEditor.tsx (1)
  • ContentEditor (20-306)
backend/src/modules/content/controllers/content.controller.ts (1)
backend/src/modules/storage/services/storage.service.ts (1)
  • StorageService (49-250)
frontend/src/pages/content/ContentEditor.tsx (2)
frontend/src/scripts/api.ts (1)
  • apiURL (8-8)
backend/src/shared/config/firebase.config.ts (1)
  • auth (31-31)
backend/src/app.config.ts (1)
backend/src/shared/config/environment.ts (1)
  • env (28-64)
backend/src/modules/storage/services/storage.service.ts (1)
backend/src/shared/utils/logger.ts (1)
  • logger (14-36)
🔇 Additional comments (14)
backend/src/modules/subscription/services/stripe.service.ts (1)

6-6:

Details

✅ Verification successful

Verify the Stripe API version update for compatibility and breaking changes.

The API version was updated from '2025-04-30.basil' to '2025-05-28.basil'. Please ensure this version exists and verify there are no breaking changes that could affect the existing Stripe service functionality.


🌐 Web query:

Is Stripe API version 2025-05-28.basil valid and what are the differences between 2025-04-30.basil and 2025-05-28.basil?

💡 Result:

Yes, Stripe API version 2025-05-28.basil is valid. This version is part of Stripe's "Basil" release, which follows their new API release process introduced in 2024. Under this process, major releases occur twice a year and include breaking changes, while monthly releases add new features without breaking changes. (stripe.com)

The "Basil" release began with version 2025-03-31.basil, introducing several breaking changes and new features. Subsequent versions, including 2025-04-30.basil and 2025-05-28.basil, have been part of this release cycle, focusing on additive changes without introducing new breaking changes.

While the specific changelog for 2025-05-28.basil isn't detailed in the provided sources, it's important to note that this version would include only additive changes compared to 2025-04-30.basil, as per Stripe's versioning policy. For a comprehensive list of changes and to ensure your integration remains up-to-date, you can refer to Stripe's changelog. (docs.stripe.com)

To stay informed about the latest updates and to plan your integration accordingly, regularly reviewing Stripe's changelog is recommended.

Citations:


Stripe API version update verified – no breaking changes

The new API version 2025-05-28.basil is valid and, per Stripe’s “Basil” release policy, only includes additive (non-breaking) changes compared to 2025-04-30.basil. Your existing integration should continue to work without modification.

• No breaking changes introduced in this monthly release
• Version exists and is part of Stripe’s documented release cycle
• For full details, you may review Stripe’s changelog: https://docs.stripe.com/changelog?utm_source=openai

backend/package.json (2)

35-36: LGTM! AWS SDK packages added for S3 integration.

The addition of @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner packages at version 3.816.0 is appropriate for implementing AWS S3 storage functionality as mentioned in the PR objectives.


20-20:

Details

❓ Verification inconclusive

Verify @types/express version update for breaking changes.

The @types/express version was updated from 4.17.1 to 4.17.22, which is a significant jump that may include breaking changes or new type definitions.

Please ensure all Express-related TypeScript code still compiles correctly with the updated type definitions.


🏁 Script executed:

#!/bin/bash
# Check for TypeScript compilation issues after @types/express update
find backend/src -name "*.ts" -exec grep -l "express" {} \; | head -10

Length of output: 1200


Ensure Express types still compile after updating @types/express
The @types/express entry in backend/package.json was bumped from 4.17.1 → 4.17.22. Please confirm there are no new type errors before merging:

  • Run a full type‐check in the backend folder:
    cd backend
    npm run build   # or npx tsc --noEmit
  • Locate every Express import and verify usage sites:
    rg "from 'express'" -n backend/src
  • Fix any reported compilation failures.
backend/src/modules/user/controllers/user.controller.ts (1)

146-151: LGTM! Upload integration with StorageService looks correct.

The file upload implementation correctly integrates with the updated StorageService, passing all required parameters including the fileType parameter. This aligns well with the storage service refactoring mentioned in the relevant code snippets.

frontend/src/App.tsx (1)

97-98: LGTM! Clear routing separation for create vs edit modes.

The explicit isEditMode prop makes the component behavior clear and the new /content/edit/:id route follows REST conventions. This aligns well with the ContentEditor component changes that handle both creation and editing workflows.

backend/src/app.ts (2)

8-8: Good addition for filesystem path operations.

The path import is needed for the static file serving middleware that serves local uploads during development.


64-67:

Details

✅ Verification successful

Static file serving for development uploads looks good.

The middleware correctly serves files from the local_uploads directory at the /local_uploads route, enabling access to locally stored files during development. This aligns with the dual-mode storage strategy.

Verify that the local_uploads directory is properly excluded from version control:


🏁 Script executed:

#!/bin/bash
# Description: Check if local_uploads directory is ignored in .gitignore
# Expected: local_uploads should be listed in .gitignore

if [ -f .gitignore ]; then
    if grep -q "local_uploads" .gitignore; then
        echo "✅ local_uploads is properly ignored in .gitignore"
        grep "local_uploads" .gitignore
    else
        echo "❌ local_uploads is NOT found in .gitignore - this could lead to committed uploads"
    fi
else
    echo "❌ .gitignore file not found"
fi

Length of output: 213


Static file serving and VCS exclusion verified

  • The app.use("/local_uploads", express.static(...)) middleware correctly serves files from the local_uploads directory.
  • Confirmed that local_uploads is listed in .gitignore, preventing accidental commits of uploaded files.
backend/src/app.config.ts (1)

19-27:

Details

✅ Verification successful

Enhanced CSP configuration supports image serving functionality.

The Content Security Policy updates are well-configured:

  • img-src directive allowing localhost:3000 enables serving development images
  • data: support allows inline/base64 images
  • cross-origin resource policy enables frontend access to backend-served images

Verify the frontend URL configuration matches the CSP allowlist:


🏁 Script executed:

#!/bin/bash
# Description: Check if the frontend URL in environment matches the CSP img-src directive
# Expected: Frontend URL should be localhost:3000 or match the CSP configuration

echo "Checking environment configuration..."
if [ -f ".env" ]; then
    grep -E "(FRONTEND_URL|PORT)" .env || echo "No frontend URL or port found in .env"
else
    echo "No .env file found"
fi

echo -e "\nChecking if the CSP img-src directive matches expected frontend URLs..."
rg -A 5 "img-src" backend/src/app.config.ts

Length of output: 540


CSP img-src directive correctly includes http://localhost:3000

  • Verification complete: the snippet in backend/src/app.config.ts
    "img-src": ["'self'", "http://localhost:3000", "data:"],
    
    matches the intended development URL.
  • No .env file was found in the repo; if you rely on a FRONTEND_URL variable in other environments, ensure its value is added to this CSP allowlist before deployment.
backend/src/modules/content/controllers/content.controller.ts (3)

36-44: Excellent input validation for file uploads.

The validation correctly checks for:

  • Existence of files.thumbnail
  • Array structure validation
  • Presence of at least one file

This prevents runtime errors and provides clear feedback to clients about missing uploads.


51-57: Good file path validation and StorageService integration.

The file path validation prevents errors from missing file data. The file object modification with { ...file, path: filePath } ensures compatibility with the StorageService.uploadFile method that expects a path property.


151-153: Consistent file validation in edit method.

The validation matches the pattern used in uploadThumbnail and prevents processing when no file is uploaded during content editing.

frontend/src/pages/content/ContentView.tsx (3)

41-41: LGTM! Consistent refactoring approach.

The change from destructured user to auth object provides better flexibility and consistency throughout the component.


470-471: Good fix for the navigation path!

Adding the leading slash ensures absolute path navigation, preventing potential routing issues.


475-480: Excellent security improvement!

The refactored isCreator function is more secure and reliable by:

  • Directly comparing UIDs instead of relying on localStorage (which can be manipulated)
  • Adding proper null safety checks
  • Using the authenticated user from the auth context

Comment on lines +234 to 236
if (auth.user?.uid) {
navigate(`/profile/${auth.user.uid}`);
} else {
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.

⚠️ Potential issue

Add null safety checks for auth.user access.

In several places, auth.user.uid and auth.user.username are accessed without optional chaining, which could cause runtime errors if the user is null.

Apply optional chaining to prevent potential runtime errors:

-      navigate(`/profile/${auth.user.uid}`);
+      navigate(`/profile/${auth.user?.uid}`);
-                userId: auth.user.uid,
-                username: auth.user.username,
+                userId: auth.user?.uid,
+                username: auth.user?.username,

Also applies to: 267-268, 356-357, 382-383

🤖 Prompt for AI Agents
In frontend/src/pages/content/ContentView.tsx at lines 234-236, 267-268,
356-357, and 382-383, the code accesses auth.user.uid and auth.user.username
without optional chaining, risking runtime errors if auth.user is null. Update
these lines to use optional chaining (e.g., auth.user?.uid and
auth.user?.username) to safely access these properties only when auth.user is
defined.

Comment on lines +132 to +196
const handleSubmit = async () => {
setError("");

if (!title || !content) {
setError(
"Please add some content before summarizing using our AI service."
"Title and content are required, and were not provided. Please try again."
);
return;
}

setIsSummarizing(true);
try {
const response = await fetch(`${apiURL}/api/v1/summarize`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ input: content }),
});

const data = await response.json();
if (!response.ok) {
setError(
data.error ||
"Failed to summarize provided content. Please Try again."
// Prepare content payload
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const newContent: Record<string, any> = {
creatorUID: auth.user!.uid,
title,
content,
};

// Handle thumbnail upload if present
if (thumbnail) {
const formData = new FormData();
formData.append("thumbnail", thumbnail);
const thumbRes = await axios.post(
`${apiURL}/content/uploadThumbnail`,
formData,
{
headers: { "Content-Type": "multipart/form-data" },
}
);
newContent.thumbnailUrl = thumbRes.data.url;
}

if (!editor) {
return;
}

const formattedSummary = `
<div class="summary-container">
<h2 class="summary-title">Summary</h2>
<div class="summary-content">
<p>${data.summary.output.replace(/\n/g, "</p><p>")}</p>
</div>
</div>
`;

editor.commands.setContent(formattedSummary);
setContent(formattedSummary);
Cookies.set("content", formattedSummary);
} catch (error) {
setError(
`Failed to summarize provided content: ${
(error as Error).message
}, something went wrong. Please Try again.`
);
}

setIsSummarizing(false);
};

/**
* handleSubmit() -> void
*
* @description
* Handles the submission of the content, setting { title, content, thumbnail }
* respectively amnd redirecting to the Content page. If the title and content
* are not provided, it will throw an error and set the error state to the current
* error message based on the error thrown.
*
* @returns
*/
function handleSubmit() {
setError("");

if (title === "" || content === "") {
setError(
"Title and content are required, and were not provided. Please Try again."
);
return;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const newContent: Record<string, any> = {
creatorUID: user!.uid,
title,
content,
};

if (thumbnail) {
const formData = new FormData();
formData.append("thumbnail", thumbnail);

fetch(`${apiURL}/content/uploadThumbnail`, {
method: "POST",
body: formData,
})
.then(async (response) => {
const res = await response.json();
const thumbnailUrl = res.url;
newContent["thumbnailUrl"] = thumbnailUrl;

return fetch(`${apiURL}/content`, {
method: "POST",
// Determine request method and URL
let response;
if (isEditMode) {
const contentId = window.location.pathname.split("/").pop();
response = await axios.put(
`${apiURL}/content/${contentId}`,
newContent,
{
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newContent),
});
})
.then(async (response) => {
if (response.status === 200 || response.status === 201) {
Cookies.remove("content");
localStorage.removeItem("title");
navigate("/");
} else {
setError("Failed to create content. Please Try again.");
withCredentials: true,
}
})
.catch((error) => {
console.log(error);
setError("Failed to create content. Please Try again.");
});
} else {
fetch(`${apiURL}/content`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newContent),
})
.then((response) => response.json())
.then(async () => {
Cookies.remove("content");
localStorage.removeItem("title");
navigate("/");
})
.catch((error) => {
console.log(error);
setError("Failed to create content. Please Try again.");
);
} else {
response = await axios.post(`${apiURL}/content`, newContent, {
headers: { "Content-Type": "application/json" },
withCredentials: true,
});
}

// Notify followers if content creation/update was successful
if (response.status === 200 || response.status === 201) {
Cookies.remove("content");
localStorage.removeItem("title");
navigate("/");
} else {
setError("Failed to save content. Please try again.");
}
} catch (err) {
console.error(err);
setError("Failed to save content. Please try again.");
}
}
};
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.

⚠️ Potential issue

Add safety checks and improve content ID extraction.

The handleSubmit function has potential runtime issues with null assertions and unchecked property access.

Apply these safety improvements:

-        creatorUID: auth.user!.uid,
+        creatorUID: auth.user?.uid || '',

Add validation after thumbnail upload:

         );
+        if (!thumbRes.data?.url) {
+          throw new Error("Thumbnail upload failed - no URL returned");
+        }
         newContent.thumbnailUrl = thumbRes.data.url;

Use the suggested useParams approach:

-        const contentId = window.location.pathname.split("/").pop();
+        const contentId = id; // from useParams

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/src/pages/content/ContentEditor.tsx around lines 132 to 196, improve
safety by replacing the direct use of auth.user! with a check to ensure
auth.user exists before accessing uid. After uploading the thumbnail, validate
that thumbRes.data.url exists before assigning it to newContent.thumbnailUrl.
Replace the extraction of contentId from window.location.pathname with the
useParams hook from react-router to safely and reliably get the content ID.
These changes will prevent runtime errors from null or undefined values and
improve code robustness.

Comment thread frontend/src/pages/content/ContentEditor.tsx
Comment thread backend/src/modules/storage/services/storage.service.ts
Comment on lines +134 to +168
private static async uploadFileProduction(
file: File,
filePath: string,
fileName: string,
fileType: string
): Promise<{ url: string }> {
try {
const fileBuffer = await fs.readFile(file.webkitRelativePath);
const key = path.posix.join(filePath, fileName);

await s3.send(
new PutObjectCommand({
Bucket: BUCKET,
Key: key,
Body: fileBuffer,
ContentType: fileType,
})
);

// Generate a signed GET URL for the uploaded file
const url = await getSignedUrl(
s3,
new GetObjectCommand({
Bucket: BUCKET,
Key: key,
}),
{ expiresIn: 3600 }
);

return { url };
} catch (error: any) {
logger.error("S3 upload error:", error);
throw new Error(`S3 upload failed: ${error.message}`);
}
}
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.

⚠️ Potential issue

Critical: Fix file reading in uploadFileProduction.

The code uses file.webkitRelativePath which is a browser API and doesn't exist in Node.js context.

Fix the file reading logic:

-      const fileBuffer = await fs.readFile(file.webkitRelativePath);
+      let fileBuffer: Buffer;
+      if ('filepath' in file && file.filepath) {
+        fileBuffer = await fs.readFile(file.filepath);
+      } else if ('buffer' in file && file.buffer) {
+        fileBuffer = file.buffer;
+      } else {
+        throw new Error("No valid file data found");
+      }

Also consider making the signed URL expiration configurable:

-        { expiresIn: 3600 }
+        { expiresIn: parseInt(process.env.S3_SIGNED_URL_EXPIRY || '3600') }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static async uploadFileProduction(
file: File,
filePath: string,
fileName: string,
fileType: string
): Promise<{ url: string }> {
try {
const fileBuffer = await fs.readFile(file.webkitRelativePath);
const key = path.posix.join(filePath, fileName);
await s3.send(
new PutObjectCommand({
Bucket: BUCKET,
Key: key,
Body: fileBuffer,
ContentType: fileType,
})
);
// Generate a signed GET URL for the uploaded file
const url = await getSignedUrl(
s3,
new GetObjectCommand({
Bucket: BUCKET,
Key: key,
}),
{ expiresIn: 3600 }
);
return { url };
} catch (error: any) {
logger.error("S3 upload error:", error);
throw new Error(`S3 upload failed: ${error.message}`);
}
}
private static async uploadFileProduction(
file: File,
filePath: string,
fileName: string,
fileType: string
): Promise<{ url: string }> {
try {
// Replace browser-only API with Node-friendly file data handling
let fileBuffer: Buffer;
if ('filepath' in file && file.filepath) {
fileBuffer = await fs.readFile(file.filepath);
} else if ('buffer' in file && file.buffer) {
fileBuffer = file.buffer;
} else {
throw new Error("No valid file data found");
}
const key = path.posix.join(filePath, fileName);
await s3.send(
new PutObjectCommand({
Bucket: BUCKET,
Key: key,
Body: fileBuffer,
ContentType: fileType,
})
);
// Generate a signed GET URL for the uploaded file
const url = await getSignedUrl(
s3,
new GetObjectCommand({
Bucket: BUCKET,
Key: key,
}),
{ expiresIn: parseInt(process.env.S3_SIGNED_URL_EXPIRY || '3600') }
);
return { url };
} catch (error: any) {
logger.error("S3 upload error:", error);
throw new Error(`S3 upload failed: ${error.message}`);
}
}
🤖 Prompt for AI Agents
In backend/src/modules/storage/services/storage.service.ts around lines 134 to
168, the method uploadFileProduction incorrectly uses file.webkitRelativePath to
read the file, which is a browser-only property and not available in Node.js.
Replace this with reading the file from the provided filePath or fileName
parameters using Node.js fs module correctly. Additionally, modify the signed
URL generation to accept an expiration time as a configurable parameter instead
of hardcoding 3600 seconds.

Comment on lines +90 to 123
private static async uploadFileDevelopment(
file: any,
filePath: string,
fileName: string,
fileType: string
): Promise<{ url: string }> {
try {
const dir = path.join(LOCAL_UPLOAD_DIR, filePath);
await fs.mkdir(dir, { recursive: true });

const snapshot = await uploadBytes(storageRef, fileBuffer, metadata);
logger.info(`
File: ${file}
FileBuffer: ${fileBuffer}
Path: ${filePath}
FileName: ${fileName}
StorageRef: ${storageRef}
`);
logger.info("File uploaded successfully.");

// Get the download URL
const downloadURL = await getDownloadURL(snapshot.ref);
logger.info("Download URL:", downloadURL);

return { url: downloadURL };
} catch (error: any) {
let errorMessage = error.message;
let fileBuffer: Buffer;

// Remove "Firebase: " prefix from the error message
if (errorMessage.startsWith("Firebase: ")) {
errorMessage = errorMessage.replace("Firebase: ", "");
if (file.filepath) {
fileBuffer = await fs.readFile(file.filepath);
} else if (file.buffer) {
fileBuffer = file.buffer;
} else {
throw new Error("No valid file data found (no filepath or buffer).");
}
throw new Error(errorMessage);

fileName = `${fileName}${fileType ? `.${fileType.split("/")[1]}` : ""}`;

const fullPath = path.join(dir, fileName);
await fs.writeFile(fullPath, fileBuffer);
logger.info(`File saved locally at ${fullPath}`);

// Return the backend-accessible URL
const url = `http://localhost:3000/local_uploads/${filePath}/${fileName}`;
return { url };
} catch (error: any) {
logger.error("Local upload error:", error);
throw new Error(`Local upload failed: ${error.message}`);
}
}
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.

🛠️ Refactor suggestion

Improve type safety and configuration in uploadFileDevelopment.

Several improvements needed for robustness and flexibility.

  1. Replace any type with proper typing:
-    file: any,
+    file: Express.Multer.File | { buffer: Buffer; filepath?: string },
  1. Make the URL configurable:
-      const url = `http://localhost:3000/local_uploads/${filePath}/${fileName}`;
+      const baseUrl = process.env.LOCAL_STORAGE_URL || `http://localhost:${process.env.PORT || 3000}`;
+      const url = `${baseUrl}/local_uploads/${filePath}/${fileName}`;
  1. Improve file extension handling:
-      fileName = `${fileName}${fileType ? `.${fileType.split("/")[1]}` : ""}`;
+      const ext = fileType ? `.${fileType.split("/").pop()?.split("+")[0]}` : "";
+      fileName = `${fileName}${ext}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static async uploadFileDevelopment(
file: any,
filePath: string,
fileName: string,
fileType: string
): Promise<{ url: string }> {
try {
const dir = path.join(LOCAL_UPLOAD_DIR, filePath);
await fs.mkdir(dir, { recursive: true });
const snapshot = await uploadBytes(storageRef, fileBuffer, metadata);
logger.info(`
File: ${file}
FileBuffer: ${fileBuffer}
Path: ${filePath}
FileName: ${fileName}
StorageRef: ${storageRef}
`);
logger.info("File uploaded successfully.");
// Get the download URL
const downloadURL = await getDownloadURL(snapshot.ref);
logger.info("Download URL:", downloadURL);
return { url: downloadURL };
} catch (error: any) {
let errorMessage = error.message;
let fileBuffer: Buffer;
// Remove "Firebase: " prefix from the error message
if (errorMessage.startsWith("Firebase: ")) {
errorMessage = errorMessage.replace("Firebase: ", "");
if (file.filepath) {
fileBuffer = await fs.readFile(file.filepath);
} else if (file.buffer) {
fileBuffer = file.buffer;
} else {
throw new Error("No valid file data found (no filepath or buffer).");
}
throw new Error(errorMessage);
fileName = `${fileName}${fileType ? `.${fileType.split("/")[1]}` : ""}`;
const fullPath = path.join(dir, fileName);
await fs.writeFile(fullPath, fileBuffer);
logger.info(`File saved locally at ${fullPath}`);
// Return the backend-accessible URL
const url = `http://localhost:3000/local_uploads/${filePath}/${fileName}`;
return { url };
} catch (error: any) {
logger.error("Local upload error:", error);
throw new Error(`Local upload failed: ${error.message}`);
}
}
private static async uploadFileDevelopment(
- file: any,
+ file: Express.Multer.File | { buffer: Buffer; filepath?: string },
filePath: string,
fileName: string,
fileType: string
): Promise<{ url: string }> {
try {
const dir = path.join(LOCAL_UPLOAD_DIR, filePath);
await fs.mkdir(dir, { recursive: true });
let fileBuffer: Buffer;
if (file.filepath) {
fileBuffer = await fs.readFile(file.filepath);
} else if (file.buffer) {
fileBuffer = file.buffer;
} else {
throw new Error("No valid file data found (no filepath or buffer).");
}
- fileName = `${fileName}${fileType ? `.${fileType.split("/")[1]}` : ""}`;
+ const ext = fileType
+ ? `.${fileType.split("/").pop()?.split("+")[0]}`
+ : "";
+ fileName = `${fileName}${ext}`;
const fullPath = path.join(dir, fileName);
await fs.writeFile(fullPath, fileBuffer);
logger.info(`File saved locally at ${fullPath}`);
// Return the backend-accessible URL
- const url = `http://localhost:3000/local_uploads/${filePath}/${fileName}`;
+ const baseUrl =
+ process.env.LOCAL_STORAGE_URL ||
+ `http://localhost:${process.env.PORT || 3000}`;
+ const url = `${baseUrl}/local_uploads/${filePath}/${fileName}`;
return { url };
} catch (error: any) {
logger.error("Local upload error:", error);
throw new Error(`Local upload failed: ${error.message}`);
}
}
🤖 Prompt for AI Agents
In backend/src/modules/storage/services/storage.service.ts lines 90 to 123,
replace the 'any' type for the 'file' parameter with a more specific interface
or type that includes 'filepath' and 'buffer' properties to improve type safety.
Make the base URL used in the returned 'url' configurable via environment
variables or configuration files instead of hardcoding 'http://localhost:3000'.
Refactor the file extension extraction logic to handle edge cases more robustly,
such as validating the fileType format before splitting and defaulting to a safe
extension if needed.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🔭 Outside diff range comments (1)
frontend/src/pages/content/ContentEditor.tsx (1)

131-208: ⚠️ Potential issue

Fix critical syntax errors and code structure issues.

The handleSubmit function has severe syntax errors and incomplete code that will prevent compilation. The static analysis correctly identifies parse errors.

The code mixes axios and fetch inconsistently and has broken syntax. Here's a corrected version:

-  const handleSubmit = async () => {
-    setError("");
-
-    if (!title || !content) {
-      setError(
-        "Title and content are required, and were not provided. Please try again."
-      );
-      return;
-    }
-
-    try {
-      // Prepare content payload
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      const newContent: Record<string, any> = {
-        creatorUID: auth.user!.uid,
-        title,
-        content,
-      };
-
-      // Handle thumbnail upload if present
-      if (thumbnail) {
-        const formData = new FormData();
-        formData.append("thumbnail", thumbnail);
-        const thumbRes = await axios.post(
-          `${apiURL}/content/uploadThumbnail`,
-          formData,
-          {
-            headers: { "Content-Type": "multipart/form-data" },
-          }
-        );
-        newContent.thumbnailUrl = thumbRes.data.url;
-      }
-
-      // Determine request method and URL
-      let response;
-      if (isEditMode) {
-        const contentId = window.location.pathname.split("/").pop();
-        response = await axios.put(
-          `${apiURL}/content/${contentId}`,
-          newContent,
-          {
-            headers: { "Content-Type": "application/json" },
-            withCredentials: true,
-            body: JSON.stringify(newContent),
-          });
-        })
-        .then(async (response) => {
-          if (response.status === 200 || response.status === 201) {
-            Cookies.remove("content");
-            localStorage.removeItem("title");
-            navigate("/");
-          } else {
-            setError("Failed to create content. Please Try again.");
-          }
-        );
-      } else {
-        response = await axios.post(`${apiURL}/content`, newContent, {
-          headers: { "Content-Type": "application/json" },
-          withCredentials: true,
-        });
-    } else {
-      fetch(`${apiURL}/content`, {
-        method: "POST",
-        headers: { "Content-Type": "application/json" },
-        body: JSON.stringify(newContent),
-      })
-        .then((response) => response.json())
-        .then(async () => {
-          Cookies.remove("content");
-          localStorage.removeItem("title");
-          navigate("/");
-        })
-        .catch((error) => {
-          console.log(error);
-          setError("Failed to create content. Please Try again.");
-        });
-    }
-  };
+  const handleSubmit = async () => {
+    setError("");
+
+    if (!title || !content) {
+      setError(
+        "Title and content are required, and were not provided. Please try again."
+      );
+      return;
+    }
+
+    if (!auth.user?.uid) {
+      setError("Authentication required. Please login again.");
+      return;
+    }
+
+    try {
+      // Prepare content payload
+      const newContent: Record<string, any> = {
+        creatorUID: auth.user.uid,
+        title,
+        content,
+      };
+
+      // Handle thumbnail upload if present
+      if (thumbnail) {
+        const formData = new FormData();
+        formData.append("thumbnail", thumbnail);
+        const thumbRes = await axios.post(
+          `${apiURL}/content/uploadThumbnail`,
+          formData,
+          {
+            headers: { "Content-Type": "multipart/form-data" },
+            withCredentials: true,
+          }
+        );
+        
+        if (!thumbRes.data?.url) {
+          throw new Error("Thumbnail upload failed - no URL returned");
+        }
+        newContent.thumbnailUrl = thumbRes.data.url;
+      }
+
+      // Determine request method and URL
+      let response;
+      if (isEditMode) {
+        const contentId = id; // from useParams
+        response = await axios.put(
+          `${apiURL}/content/${contentId}`,
+          newContent,
+          {
+            headers: { "Content-Type": "application/json" },
+            withCredentials: true,
+          }
+        );
+      } else {
+        response = await axios.post(`${apiURL}/content`, newContent, {
+          headers: { "Content-Type": "application/json" },
+          withCredentials: true,
+        });
+      }
+
+      if (response.status === 200 || response.status === 201) {
+        Cookies.remove("content");
+        localStorage.removeItem("title");
+        navigate("/");
+      } else {
+        setError(`Failed to ${isEditMode ? 'update' : 'create'} content. Please try again.`);
+      }
+    } catch (error) {
+      console.error(error);
+      setError(`Failed to ${isEditMode ? 'update' : 'create'} content. Please try again.`);
+    }
+  };

Key fixes:

  1. Removed syntax errors and incomplete statements
  2. Added null safety for auth.user?.uid as mentioned in past reviews
  3. Used consistent axios calls instead of mixing with fetch
  4. Added proper error handling and validation
  5. Used id from useParams instead of URL parsing
  6. Added validation for thumbnail upload response
🧰 Tools
🪛 Biome (1.9.4)

[error] 176-177: Expected a statement but instead found ')
.then(async (response) =>'.

Expected a statement here.

(parse)


[error] 185-185: Expected a statement but instead found ')'.

Expected a statement here.

(parse)


[error] 186-186: Expected a statement but instead found 'else'.

Expected a statement here.

(parse)


[error] 191-191: Expected a statement but instead found 'else'.

Expected a statement here.

(parse)


[error] 208-208: Expected a catch clause but instead found ';'.

Expected a catch clause here.

(parse)


[error] 177-186: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.

(lint/complexity/noUselessLoneBlockStatements)


[error] 186-191: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.

(lint/complexity/noUselessLoneBlockStatements)


[error] 191-207: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.

(lint/complexity/noUselessLoneBlockStatements)

♻️ Duplicate comments (1)
frontend/src/pages/content/ContentEditor.tsx (1)

66-98: ⚠️ Potential issue

Address the content ID extraction issue mentioned in past reviews.

The code still uses the fragile URL parsing approach that was flagged in previous reviews. Additionally, there's a missing axios import.

Add the missing axios import:

+import axios from 'axios';

Use React Router's useParams hook as suggested in past reviews:

-import { useNavigate } from "react-router-dom";
+import { useNavigate, useParams } from "react-router-dom";

Then in the component:

+  const { id } = useParams<{ id: string }>();

And replace the fragile URL parsing:

-          const content = await axios.get(
-            `${apiURL}/content/${window.location.pathname.split("/").pop()}`,
+          const content = await axios.get(
+            `${apiURL}/content/${id}`,
🧹 Nitpick comments (1)
frontend/src/pages/content/ContentEditor.tsx (1)

114-125: Improve error handling for thumbnail changes.

Consider clearing the error state when a valid image is selected to provide better user feedback.

  const handleThumbnailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files ? e.target.files[0] : null;

    if (file && file.type.startsWith("image/")) {
      setThumbnail(file);
      setThumbnailPreview(URL.createObjectURL(file));
+     setError(""); // Clear any previous errors
    } else {
      setThumbnail(null);
      setThumbnailPreview(null);
      setError("Please select a valid image file.");
    }
  };
🧰 Tools
🪛 Biome (1.9.4)

[error] 117-117: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 19ccb57 and e2871ee.

📒 Files selected for processing (6)
  • .gitignore (1 hunks)
  • backend/package.json (2 hunks)
  • backend/src/app.config.ts (1 hunks)
  • backend/src/app.ts (2 hunks)
  • frontend/src/pages/content/ContentEditor.tsx (7 hunks)
  • frontend/src/pages/content/ContentView.tsx (10 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • backend/package.json
  • .gitignore
  • backend/src/app.config.ts
  • backend/src/app.ts
  • frontend/src/pages/content/ContentView.tsx
🧰 Additional context used
🪛 Biome (1.9.4)
frontend/src/pages/content/ContentEditor.tsx

[error] 185-185: Expected a statement but instead found ')'.

Expected a statement here.

(parse)


[error] 186-186: Expected a statement but instead found 'else'.

Expected a statement here.

(parse)


[error] 208-208: Expected a catch clause but instead found ';'.

Expected a catch clause here.

(parse)

🔇 Additional comments (2)
frontend/src/pages/content/ContentEditor.tsx (2)

19-19: LGTM! Good addition of the isEditMode prop.

The component signature change to accept an isEditMode boolean prop effectively unifies the creation and editing workflows.


221-221: LGTM! Good dynamic title implementation.

The conditional title rendering appropriately reflects whether the user is creating or editing content.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
frontend/src/pages/content/ContentEditor.tsx (1)

67-100: Address the fragile content ID extraction and improve safety.

This code segment has the same issues flagged in previous reviews that need to be addressed.

The current approach still uses window.location.pathname.split("/").pop() for content ID extraction, which is fragile and was flagged in previous reviews. You should implement the suggested useParams approach.

Additionally, ensure proper error handling and consider the property name consistency issue mentioned in past comments.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e2871ee and 8b5d632.

📒 Files selected for processing (1)
  • frontend/src/pages/content/ContentEditor.tsx (4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
frontend/src/pages/content/ContentEditor.tsx (2)
frontend/src/scripts/api.ts (1)
  • apiURL (8-8)
backend/src/shared/config/firebase.config.ts (1)
  • auth (31-31)
🔇 Additional comments (4)
frontend/src/pages/content/ContentEditor.tsx (4)

18-18: Good addition of axios import.

The axios import is correctly added to support the new async API calls in the component.


20-20: Well-designed prop interface for mode switching.

The component signature change to accept isEditMode properly enables the component to handle both creation and editing scenarios.


25-25: Good addition of error state management.

Adding error state enables proper user feedback for API failures and validation issues.


227-227: Clean dynamic title implementation.

The conditional title rendering properly reflects the component's current mode.

Comment on lines +197 to +219
// If submission was successful, redirect to the content page
if (!error) {
// Reset local storage and cookies
localStorage.removeItem("title");
Cookies.remove("content");
setTitle("");
setContent("");

const contentId = isEditMode
? window.location.pathname.split("/").pop()
: response?.data?.id; // Get the new content ID from the response
if (contentId) {
navigate(`/content/${contentId}`);
} else {
setError("Content ID not found after submission. Please try again.");
}
}
}

// User must be authenticated to create content
if (!auth.isAuthenticated) {
navigate("/authentication/login");
}
// User must be authenticated to create content
if (!auth.isAuthenticated) {
navigate("/authentication/login");
}
};
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.

⚠️ Potential issue

Fix logic flow and improve error handling.

The current logic has several issues with error handling and authentication placement.

Issues:

  1. Authentication check should be at the beginning, not after submission
  2. Error check logic is inverted
  3. Same fragile URL parsing issue

Apply this fix:

- // If submission was successful, redirect to the content page
- if (!error) {
+ // If submission was successful, redirect to the content page  
+ if (response) {
    // Reset local storage and cookies
    localStorage.removeItem("title");
    Cookies.remove("content");
    setTitle("");
    setContent("");

    const contentId = isEditMode
      ? window.location.pathname.split("/").pop()
      : response?.data?.id;
    if (contentId) {
      navigate(`/content/${contentId}`);
    } else {
      setError("Content ID not found after submission. Please try again.");
    }
  }
-
- // User must be authenticated to create content
- if (!auth.isAuthenticated) {
-   navigate("/authentication/login");
- }

Move the authentication check to the beginning of the function as shown in the previous comment.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/src/pages/content/ContentEditor.tsx around lines 197 to 219, move
the authentication check to the very start of the function to prevent
unauthorized submissions. Correct the error handling logic so that it properly
checks for errors before proceeding. Replace the fragile URL parsing method with
a more reliable way to obtain the content ID, such as using route parameters or
a safer extraction method. Ensure the flow first verifies authentication, then
handles errors correctly, and finally processes the content ID for navigation.

Comment thread frontend/src/pages/content/ContentEditor.tsx
BengeeL and others added 3 commits May 30, 2025 14:53
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🔭 Outside diff range comments (1)
frontend/src/pages/content/ContentEditor.tsx (1)

3-3: 🛠️ Refactor suggestion

Implement useParams for safer content ID extraction.

Add the useParams import to replace fragile URL parsing used throughout the component.

-import { useNavigate } from "react-router-dom";
+import { useNavigate, useParams } from "react-router-dom";
♻️ Duplicate comments (6)
backend/src/modules/storage/services/storage.service.ts (4)

94-99: Fix type safety issues in development upload method.

The file parameter still uses the any type, which was flagged in previous reviews. This reduces type safety and IntelliSense support.

Apply this fix for better type safety:

  private static async uploadFileDevelopment(
-    file: any,
+    file: Express.Multer.File | { buffer: Buffer; filepath?: string },
    filePath: string,
    fileName: string,
    fileType: string
  ): Promise<{ url: string }> {

121-121: Make local storage URL configurable.

The hardcoded localhost URL was flagged in previous reviews and remains unaddressed. This reduces flexibility across different development environments.

Apply this fix to make the URL configurable:

-      const url = `http://localhost:3000/local_uploads/${filePath}/${fileName}`;
+      const baseUrl = process.env.LOCAL_STORAGE_URL || `http://localhost:${process.env.PORT || 3000}`;
+      const url = `${baseUrl}/local_uploads/${filePath}/${fileName}`;

145-145: CRITICAL: Fix file reading in production method.

The code still uses file.webkitRelativePath which is a browser-only API and doesn't exist in Node.js backend context. This will cause runtime errors in production.

Apply this critical fix:

-      const fileBuffer = await fs.readFile(file.webkitRelativePath);
+      let fileBuffer: Buffer;
+      if ('filepath' in file && file.filepath) {
+        fileBuffer = await fs.readFile(file.filepath);
+      } else if ('buffer' in file && file.buffer) {
+        fileBuffer = file.buffer;
+      } else {
+        throw new Error("No valid file data found");
+      }

164-164: Make S3 signed URL expiration configurable.

The hardcoded 3600 seconds expiration was flagged in previous reviews for lacking configurability.

Apply this improvement:

-        { expiresIn: 3600 }
+        { expiresIn: parseInt(process.env.S3_SIGNED_URL_EXPIRY || '3600') }
frontend/src/pages/content/ContentEditor.tsx (2)

144-231: CRITICAL: Fix multiple safety issues in handleSubmit function.

This function has several critical issues that were flagged in previous reviews:

  1. Unsafe auth access: auth.user!.uid can cause runtime errors
  2. Authentication check placement: Should be at the beginning
  3. Missing thumbnail validation: No check if upload succeeded
  4. Fragile URL parsing: Using window.location.pathname.split("/").pop()

Apply these critical fixes:

  const handleSubmit = async () => {
+   // Check authentication first
+   if (!auth.isAuthenticated || !auth.user) {
+     navigate("/authentication/login");
+     return;
+   }
+
    setError("");

    if (!title || !content) {
      setError(
        "Title and content are required, and were not provided. Please try again."
      );
      return;
    }

    const newContent: Record<string, any> = {
-     creatorUID: auth.user!.uid,
+     creatorUID: auth.user.uid,
      title,
      content,
    };

    let response;

    try {
      if (thumbnail) {
        const formData = new FormData();
        formData.append("thumbnail", thumbnail);
        const thumbRes = await axios.post(
          `${apiURL}/content/uploadThumbnail`,
          formData,
          {
            headers: { "Content-Type": "multipart/form-data" },
          }
        );
+       if (!thumbRes.data?.url) {
+         throw new Error("Thumbnail upload failed - no URL returned");
+       }
        newContent.thumbnailUrl = thumbRes.data.url;
      }

      if (isEditMode) {
-       const contentId = window.location.pathname.split("/").pop();
+       const contentId = id;
        response = await axios.put(
          `${apiURL}/content/${contentId}`,
          newContent,
          {
            headers: { "Content-Type": "application/json" },
            withCredentials: true,
          }
        );
      } else {
        // ... rest of POST logic
      }
    } catch (error) {
      console.error("Error submitting content:", error);
      setError(
        "An error occurred while submitting the content. Please try again."
      );
+     return; // Early return on error
    }

    // If submission was successful, redirect to the content page
-   if (!error) {
+   if (response) {
      // Reset local storage and cookies
      localStorage.removeItem("title");
      Cookies.remove("content");
      setTitle("");
      setContent("");

      const contentId = isEditMode
-       ? window.location.pathname.split("/").pop()
+       ? id
        : response?.data?.id;
      if (contentId) {
        navigate(`/content/${contentId}`);
      } else {
        setError("Content ID not found after submission. Please try again.");
      }
    }

-   // User must be authenticated to create content
-   if (!auth.isAuthenticated) {
-     navigate("/authentication/login");
-   }
  };

20-20: 🛠️ Refactor suggestion

Add useParams hook for content ID extraction.

Extract the content ID using React Router's useParams instead of parsing window.location.

export default function ContentEditor({ isEditMode }: { isEditMode: boolean }) {
+  const { id } = useParams<{ id: string }>();
   const [title, setTitle] = useState("");
🧹 Nitpick comments (1)
frontend/src/pages/content/ContentEditor.tsx (1)

127-138: Minor: Clear error state on valid thumbnail selection.

Consider clearing the error state when a valid thumbnail is selected to improve user experience.

  const handleThumbnailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files ? e.target.files[0] : null;

    if (file && file.type.startsWith("image/")) {
      setThumbnail(file);
      setThumbnailPreview(URL.createObjectURL(file));
+     setError(""); // Clear any previous error
    } else {
      setThumbnail(null);
      setThumbnailPreview(null);
      setError("Please select a valid image file.");
    }
  };
🧰 Tools
🪛 Biome (1.9.4)

[error] 127-127: expected , but instead found const

Remove const

(parse)


[error] 130-130: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8b5d632 and 092510d.

📒 Files selected for processing (2)
  • backend/src/modules/storage/services/storage.service.ts (1 hunks)
  • frontend/src/pages/content/ContentEditor.tsx (4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
frontend/src/pages/content/ContentEditor.tsx (2)
frontend/src/scripts/api.ts (1)
  • apiURL (8-8)
backend/src/shared/config/firebase.config.ts (1)
  • auth (31-31)
🪛 Biome (1.9.4)
frontend/src/pages/content/ContentEditor.tsx

[error] 68-68: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 69-69: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 73-73: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 80-80: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 82-82: expected , but instead found const

Remove const

(parse)


[error] 82-82: expected , but instead found (

Remove (

(parse)


[error] 87-87: expected , but instead found ;

Remove ;

(parse)


[error] 95-95: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 97-97: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 103-103: Expected a statement but instead found 'catch'.

Expected a statement here.

(parse)


[error] 106-106: Expected a catch clause but instead found ';'.

Expected a catch clause here.

(parse)


[error] 110-110: Expected a statement but instead found ', [isEditMode, editor])'.

Expected a statement here.

(parse)


[error] 71-71: Unexpected empty object pattern.

(lint/correctness/noEmptyPattern)


[error] 69-69: Shouldn't redeclare 'useNavigate'. Consider to delete it or rename it.

'useNavigate' is defined here:

(lint/suspicious/noRedeclare)


[error] 103-105: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.

(lint/complexity/noUselessLoneBlockStatements)

🔇 Additional comments (1)
backend/src/modules/storage/services/storage.service.ts (1)

30-42: LGTM: Environment validation improvements implemented.

The environment validation has been properly moved before S3 client initialization and now correctly handles development vs production scenarios. This addresses the previously identified runtime error concerns.

Comment thread frontend/src/pages/content/ContentEditor.tsx Outdated
fileType: string
): Promise<{ url: string }> {
try {
const fileBuffer = await fs.readFile(file.webkitRelativePath);
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.

Coderabbit is correct in the concern for this way of accessing the relative path. Firefox on Android is not supported, which can lead to potential issues down the road with those users. This can be addressed or we can choose to not support Android Firefox.

image

@BengeeL BengeeL merged commit cef4406 into dev Jun 1, 2025
1 check passed
@BengeeL BengeeL deleted the SUMMA-34 branch June 1, 2025 14:06
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.

2 participants