Skip to content

Feat/api security updates - #34

Merged
yash-pouranik merged 12 commits into
mainfrom
feat/api-security-updates
Mar 4, 2026
Merged

Feat/api security updates#34
yash-pouranik merged 12 commits into
mainfrom
feat/api-security-updates

Conversation

@yash-pouranik

@yash-pouranik yash-pouranik commented Mar 4, 2026

Copy link
Copy Markdown
Member

🚀 feature: Security Hardening & Release Management System

Overview

This PR introduces a comprehensive security overhaul for the platform, including granular API key management, per-project CORS/Rate Limiting, and a robust background-processing system for platform releases and mass email notifications.


🔒 Security Enhancements

1. Granular API Keys (RBAC)

  • Dual-Key Architecture: Replaced the single API key with Publishable (prefixed pk_live_) and Secret (prefixed sk_live_) keys.
  • Access Control: Implemented requireSecretKey middleware to physically block data-destructive actions when a Publishable key is used.

2. Per-Project Allowed Domains (CORS)

  • Origin Enforcement: Developers can now whitelist specific domains (e.g., https://myapp.com) in their project settings.
  • Publishable Key Protection: The backend now strictly enforces Origin checks for all requests using Publishable keys, throwing a 403 Forbidden for unauthorized domains.

3. Per-Project Rate Limiting

  • Scalable Throttling: Implemented a custom rate limiter using express-rate-limit that tracks limits per ProjectID rather than User IP.
  • Default Limit: 500 requests per 15 minutes, preventing API abuse on a per-project basis.

📢 Release & Email System

1. Public Changelog

  • New Page: /releases displays all platform updates in a modern, chronological timeline.
  • Admin Access: Securely restricted the creation of releases to yashpouranik124@gmail.com.

2. Throttled Mass Email Queue (BullMQ)

  • Reliability: Integrated BullMQ and Redis to handle mass email notifications.
  • Rate-Limit Compliance: Emails are drip-fed at a rate of 1 per 15 minutes to strictly comply with Resend’s free tier (100 daily limit).
  • Resend v2: Switched to apps.bitbros.in domain and RESEND_API_KEY_2 for improved deliverability.

3. Premium Email UI

  • Supabase Aesthetic: Both OTP and Release emails have been redesigned with a premium, minimalist layout inspired by modern developer platforms.

🐛 Bug Fixes

  • Double OTP Patch: Resolved an issue where registration was triggering two simultaneous OTP emails by removing a redundant request in the Signup.jsx lifecycle.

🛠️ Technical Refactors

  • Queue Architecture: Moved queue and worker logic to a dedicated backend/queues/ directory for better maintainability.
  • Service Decoupling: Separated sendOtp and sendReleaseEmail logic within emailService.js.

Summary by CodeRabbit

  • New Features

    • Changelog page and admin release creation UI; publishing enqueues notification emails.
    • Dual API key support (publishable + secret) with per-key regeneration UI and allowed-domains management.
  • Improvements

    • Per-project rate limiting and stricter origin validation for publishable keys.
    • Signup/OTP flow streamlined and clearer key-related UX text.
    • Landing page animations and new pricing/cta sections.
  • Chores

    • Background queue/email delivery, test mocks, and dependency updates.

@vercel

vercel Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
ur-backend Ready Ready Preview, Comment Mar 4, 2026 3:38pm

@coderabbitai

coderabbitai Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Release feature (model, controllers, routes, frontend pages) with queued release emails; initializes email queue in app bootstrap and mounts releases route; splits Project API key into separate publishable and secret keys with origin checks and per-key regeneration; introduces project-scoped rate limiting and require-secret-key middleware; integrates these across routes and updates email queue and service.

Changes

Cohort / File(s) Summary
Release feature
backend/models/Release.js, backend/controllers/release.controller.js, backend/routes/releases.js, frontend/src/pages/Releases.jsx, frontend/src/pages/AdminCreateRelease.jsx, backend/app.js, frontend/src/App.jsx
Add Release model, controllers (getAllReleases, createRelease), routes mounted at /api/releases, initialize email queue in app bootstrap, frontend listing and admin-create pages; admin create enqueues release email jobs.
Email queue & service
backend/queues/emailQueue.js, backend/utils/emailService.js, backend/package.json, backend/tests/jest.setup.js
Introduce BullMQ-backed email-queue and Worker (rate-limited), new sendReleaseEmail, update sendOtp signature/exports, add bullmq dep and test mocks for Redis/BullMQ.
API key model & verify logic
backend/models/Project.js, backend/middleware/verifyApiKey.js, backend/utils/api.js, backend/controllers/project.controller.js
Replace single apiKey with publishableKey and secretKey; generateApiKey accepts prefix; verify middleware detects key type, enforces origin checks for publishable keys, sets req.keyRole; controllers hash/store per-key, omit hashed keys from responses, and add per-key regeneration.
Project rate limiter & secret enforcement
backend/middleware/projectRateLimiter.js, backend/middleware/requireSecretKey.js
Add project-scoped rate limiter (default 500/15min, per-project override) and requireSecretKey middleware blocking non-secret key access to sensitive write/delete routes.
Route integrations
backend/routes/data.js, backend/routes/schemas.js, backend/routes/storage.js, backend/routes/projects.js, backend/app.js
Apply projectRateLimiter broadly; add requireSecretKey to sensitive routes; add PATCH /:projectId/allowed-domains; mount releases router and require email queue initialization in app bootstrap.
Domain management & cache updates
backend/controllers/project.controller.js, backend/controllers/schema.controller.js, backend/models/Project.js, backend/routes/projects.js
Add allowedDomains field (default ['*']); implement updateAllowedDomains endpoint; adjust cache invalidation to target publishableKey/secretKey; responses omit key fields.
Frontend key & domain UIs
frontend/src/pages/ProjectDetails.jsx, frontend/src/pages/CreateProject.jsx, frontend/src/pages/ProjectSettings.jsx, frontend/src/components/TryItPanel.jsx, frontend/src/components/Layout/Sidebar.jsx
UI changes to show separate publishable/secret keys, per-key regeneration flows, AllowedDomains management form (duplicated implementation present), TryIt placeholder updates, and Changelog sidebar link.
Auth & signup, config, footer
backend/controllers/auth.controller.js, frontend/src/pages/Signup.jsx, frontend/src/config.js, frontend/src/components/Layout/Footer.jsx
Switch to named import for sendOtp (export change); signup flow no longer calls /send-otp explicitly; add ADMIN_EMAIL export and use it in Footer.
Schema & storage routes
backend/controllers/schema.controller.js, backend/routes/schemas.js, backend/routes/storage.js
Apply projectRateLimiter (and requireSecretKey where appropriate) to schema/storage endpoints; ensure schema controller removes publishable/secret keys from responses and invalidates corresponding caches.
Frontend deps & tests
frontend/package.json, backend/tests/jest.setup.js
Add react-markdown, remark-gfm, framer-motion to frontend deps; add Jest mocks for ioredis and bullmq in backend tests.

Sequence Diagram(s)

sequenceDiagram
    actor Admin
    participant FE as Frontend App
    participant API as Release API (/api/releases)
    participant Ctrl as Release Controller
    participant DB as MongoDB
    participant Queue as BullMQ Queue
    participant Worker as Email Worker
    participant EmailSvc as Resend Email Service

    Admin->>FE: submit release (version, title, content)
    FE->>API: POST /api/releases (Bearer token)
    API->>Ctrl: authorize & validate
    Ctrl->>DB: save Release
    Ctrl->>DB: query verified developers
    loop per recipient
        Ctrl->>Queue: add job (email, version, title, content)
    end
    Ctrl-->>FE: 201 Created (queuedCount)

    Note over Queue,Worker: asynchronous processing (rate-limited)
    Queue->>Worker: deliver job
    Worker->>EmailSvc: sendReleaseEmail(email, details)
    EmailSvc-->>Worker: success/failure
    Worker-->>Queue: complete/fail
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

Poem

🐰 I hopped to split the keys in two,

pk for browsers, sk for the crew.
Releases queued with a BullMQ hum,
Domains, limits, and a secret-only drum.
Carrots clapped — the changelog's come!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat/api security updates' directly aligns with the PR's primary objective: implementing security hardening through granular API keys, per-project CORS, and per-project rate limiting.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/api-security-updates

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 and usage tips.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the platform's security posture and introduces a robust system for managing and communicating platform updates. It provides developers with finer control over API key usage and access, protects projects from API abuse through rate limiting and CORS enforcement, and streamlines the process of announcing new features and bug fixes to the user base via an integrated changelog and throttled email system.

Highlights

  • Granular API Key Management: Implemented a dual-key architecture with Publishable (pk_live_) and Secret (sk_live_) API keys, enhancing security by restricting data-destructive actions to Secret keys only. Project creation and regeneration flows have been updated to support these new key types.
  • Per-Project CORS (Allowed Domains): Introduced the ability for developers to whitelist specific domains for their projects. The backend now strictly enforces Origin checks for requests made with Publishable keys, returning a 403 Forbidden for unauthorized domains.
  • Per-Project Rate Limiting: Integrated a custom rate limiter using express-rate-limit that tracks API usage per ProjectID instead of User IP, with a default limit of 500 requests per 15 minutes to prevent abuse.
  • Public Changelog and Release Management System: Added a new /releases page to display platform updates in a chronological timeline. An admin-only interface (/admin/create-release) allows for the creation of new releases, which are then automatically queued for mass email notifications.
  • Throttled Mass Email Queue with BullMQ: Integrated BullMQ and Redis to handle mass email notifications for releases. Emails are drip-fed at a rate of 1 per 15 minutes to comply with Resend's free tier limits, ensuring reliable delivery.
  • Enhanced Email Templates: Redesigned both OTP and Release email templates with a premium, minimalist aesthetic inspired by modern developer platforms, and updated the Resend API key and domain for improved deliverability.
  • Bug Fix: Double OTP Email: Resolved an issue where two simultaneous OTP emails were triggered during registration by removing a redundant API call in the frontend signup process.
  • Backend Refactoring: Improved maintainability by moving queue and worker logic to a dedicated backend/queues/ directory and decoupling sendOtp and sendReleaseEmail logic within emailService.js.
Changelog
  • backend/app.js
    • Added a new route for releases.
  • backend/controllers/auth.controller.js
    • Updated the import statement for sendOtp to use destructuring.
  • backend/controllers/project.controller.js
    • Modified project creation to generate distinct publishable, secret, and JWT keys.
    • Updated project retrieval to include publishableKey, secretKey, and allowedDomains in the selection.
    • Refactored API key regeneration to support specific key types ('publishable' or 'secret') and ensure proper cache invalidation for both.
    • Updated cache invalidation logic to clear both publishable and secret API key caches.
    • Added a new controller function updateAllowedDomains to manage project-specific CORS settings.
  • backend/controllers/release.controller.js
    • Added a new controller to handle fetching all releases and creating new releases (admin-only functionality).
  • backend/controllers/schema.controller.js
    • Updated cache invalidation to clear both publishable and secret API key caches after schema operations.
  • backend/middleware/projectRateLimiter.js
    • Added a new middleware to implement project-specific rate limiting based on ProjectID.
  • backend/middleware/requireSecretKey.js
    • Added a new middleware to enforce the use of a 'secret' key for specific API endpoints.
  • backend/middleware/verifyApiKey.js
    • Modified API key verification to differentiate between 'publishable' and 'secret' keys based on their prefixes.
    • Updated project lookup to use the appropriate key field (publishableKey or secretKey).
    • Implemented CORS origin enforcement for 'publishable' keys, checking against allowedDomains stored in the project.
    • Stored the determined keyRole ('secret' or 'publishable') in the request object.
  • backend/models/Project.js
    • Replaced the single apiKey field with publishableKey and secretKey fields.
    • Added an allowedDomains array field to the project schema, defaulting to ['*'].
  • backend/models/Release.js
    • Added a new Mongoose model for Release documents, including fields for version, title, content, and publishedBy.
  • backend/package-lock.json
    • Updated various package dependencies, including bullmq, ioredis, semver, msgpackr, cron-parser, luxon, node-abort-controller, node-gyp-build-optional-packages, and detect-libc.
  • backend/package.json
    • Added bullmq as a new dependency and updated ioredis.
  • backend/queues/emailQueue.js
    • Added a new file to configure and manage the BullMQ email queue and worker, including rate limiting for email sending.
  • backend/routes/data.js
    • Applied requireSecretKey and projectRateLimiter middleware to POST, DELETE, and PUT data routes.
  • backend/routes/projects.js
    • Added a new PATCH route for updating project allowedDomains.
  • backend/routes/releases.js
    • Added a new file defining routes for fetching all releases and creating new releases.
  • backend/routes/schemas.js
    • Applied requireSecretKey and projectRateLimiter middleware to the POST schema route and projectRateLimiter to the GET schema route.
  • backend/routes/storage.js
    • Applied requireSecretKey and projectRateLimiter middleware to file upload and delete routes.
  • backend/utils/api.js
    • Modified the generateApiKey function to accept an optional prefix for generated keys.
  • backend/utils/emailService.js
    • Updated the sendOtp function to use a new 'from' email address and an enhanced HTML template.
    • Added a new sendReleaseEmail function with a dedicated HTML template for release notifications.
    • Exported both sendOtp and sendReleaseEmail.
  • frontend/src/App.jsx
    • Added new routes for the public changelog (/releases) and the admin release creation page (/admin/create-release).
  • frontend/src/components/Layout/Sidebar.jsx
    • Added a 'Changelog' link with a Rocket icon to the sidebar navigation.
  • frontend/src/components/TryItPanel.jsx
    • Updated the API key placeholder and error message to reflect the new dual-key system (Publishable/Secret keys).
  • frontend/src/pages/AdminCreateRelease.jsx
    • Added a new page for administrators to create and publish new platform releases, including queuing email notifications.
  • frontend/src/pages/CreateProject.jsx
    • Updated the UI to display and allow copying of both Publishable and Secret API keys upon project creation, with appropriate warnings.
  • frontend/src/pages/ProjectDetails.jsx
    • Updated the UI to display and allow regeneration of both Publishable and Secret API keys, with distinct visual cues and confirmation messages.
  • frontend/src/pages/ProjectSettings.jsx
    • Integrated a new AllowedDomainsForm component to manage project-specific CORS settings.
    • Added the AllowedDomainsForm to the project settings page.
  • frontend/src/pages/Releases.jsx
    • Added a new page to display a chronological list of platform releases (changelog).
  • frontend/src/pages/Signup.jsx
    • Removed a redundant API call to send OTP after successful user registration, streamlining the signup flow.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces significant security updates and a new release management system, including features like dual API keys, per-project CORS, rate limiting, and an email queue. However, the review identified several critical vulnerabilities: a CORS bypass, a stored XSS vulnerability in the new changelog feature, HTML injection in release emails, and a potential Denial of Service in the allowed domains configuration. Additionally, some hardcoded configuration values need to be moved to environment variables for improved security and maintainability.

Comment thread frontend/src/pages/Releases.jsx Outdated
Comment on lines +94 to +96
style={{ color: 'var(--color-text-muted)', lineHeight: '1.7', fontSize: '1.05rem' }}
dangerouslySetInnerHTML={{ __html: release.content.replace(/\n/g, '<br>') }}
/>

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.

security-high high

The Releases component uses dangerouslySetInnerHTML to render unsanitized release.content, creating a stored Cross-Site Scripting (XSS) vulnerability. An attacker with release creation privileges could inject malicious scripts. To remediate, sanitize the HTML content using a library like dompurify or a Markdown renderer that escapes HTML. Alternatively, render the content as plain text within a <pre> tag to prevent script execution while preserving formatting.

                            <pre style={{ color: 'var(--color-text-muted)', lineHeight: '1.7', fontSize: '1.05rem', whiteSpace: 'pre-wrap', fontFamily: 'inherit' }}>
                                {release.content}
                            </pre>

Comment on lines +78 to +81
if (domain.startsWith('*.')) {
const baseDomain = domain.substring(2);
return originUrl.endsWith(baseDomain);
}

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.

security-high high

The wildcard matching logic for allowedDomains uses originUrl.endsWith(baseDomain). This is insecure because it doesn't ensure that the baseDomain is a proper suffix of the origin's hostname. For example, if *.example.com is allowed, an attacker can use https://attacker-example.com to bypass the check, as it also ends with example.com.

Remediation: Ensure the origin hostname either matches the base domain exactly or ends with a dot followed by the base domain.

const Developer = require("../models/Developer");
const { emailQueue } = require("../queues/emailQueue");

const ADMIN_EMAIL = 'yashpouranik124@gmail.com';

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.

high

For security and maintainability, the admin email should not be hardcoded. This makes it difficult to change the admin user or run the application in different environments. It's recommended to move this to an environment variable.

Suggested change
const ADMIN_EMAIL = 'yashpouranik124@gmail.com';
const ADMIN_EMAIL = process.env.ADMIN_EMAIL;

Comment thread backend/utils/emailService.js Outdated
<div class="logo">urBackend</div>
<div class="badge">New Release ${version}</div>
<h1>${title}</h1>
<div class="content">${content}</div>

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.

security-medium medium

The sendReleaseEmail function directly embeds the content of a release into the HTML body of the email without any sanitization or escaping. This allows for the injection of malicious HTML or tracking pixels into emails sent to all verified developers.

Remediation: Sanitize the release content before embedding it into the email template.

Comment on lines +702 to +705
const { domains } = req.body;
if (!Array.isArray(domains)) {
return res.status(400).json({ error: "domains must be an array of strings." });
}

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.

security-medium medium

The updateAllowedDomains function validates that domains is an array, but it does not verify that its elements are strings. If a non-string element (e.g., an object) is included in the array, it will cause the verifyApiKey middleware to throw an unhandled exception when calling .startsWith() on it, effectively disabling the API for that project (Denial of Service).

Remediation: Add a check to ensure all elements in the domains array are strings: if (!domains.every(d => typeof d === 'string')) { ... }.

Comment thread backend/models/Release.js
Comment on lines +18 to +21
publishedBy: {
type: String,
default: 'yashpouranik124@gmail.com'
}

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.

medium

Hardcoding the default publishedBy email in the schema ties the data model to a specific user. A more flexible approach is to make this field required and set it dynamically in the controller based on the authenticated user who is creating the release. This ensures data integrity and correctly attributes the release to the publisher.

In release.controller.js, you would then set this field when creating the new release: const newRelease = new Release({ ..., publishedBy: req.user.email });

Suggested change
publishedBy: {
type: String,
default: 'yashpouranik124@gmail.com'
}
publishedBy: {
type: String,
required: true
}

content: ''
});

const isAdmin = user?.email === 'yashpouranik124@gmail.com';

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.

medium

The admin email is hardcoded on the frontend. While the primary authorization check is on the backend, this makes the UI logic rigid and hard to maintain. This value should be centralized in a configuration file (e.g., src/config.js) and imported where needed. This same issue is also present in frontend/src/pages/Releases.jsx.

Suggested change
const isAdmin = user?.email === 'yashpouranik124@gmail.com';
const isAdmin = user?.email === 'yashpouranik124@gmail.com'; // TODO: Move to a config file

Comment thread backend/routes/releases.js Fixed
Comment thread backend/routes/releases.js Fixed
Comment thread backend/routes/releases.js Fixed

@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: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/pages/CreateProject.jsx (1)

133-133: ⚠️ Potential issue | 🟡 Minor

Text should use plural "keys" for consistency.

The button text still uses singular "key" but there are now two keys displayed (publishable and secret). This is inconsistent with the updated warning text at lines 97-99 that correctly uses "keys" (plural).

✏️ Suggested fix
-                        I have saved the key, Go to Dashboard
+                        I have saved the keys, Go to Dashboard
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/CreateProject.jsx` at line 133, Update the button text in
the CreateProject component that currently reads "I have saved the key, Go to
Dashboard" to use plural "keys" for consistency with the warning copy; locate
the JSX button/label in CreateProject.jsx (the element rendering that exact
string) and change it to "I have saved the keys, Go to Dashboard" (or similar
pluralized phrasing).
🧹 Nitpick comments (3)
frontend/src/pages/ProjectSettings.jsx (1)

926-938: Add lightweight client-side domain format validation before PATCH.

Current add flow only checks duplicates; validating *, *.domain.com, or full http(s) URLs will reduce invalid allowlist updates.

💡 Suggested refactor
   const addDomain = () => {
     const domain = newDomain.trim();
     if (!domain) return;

+    const isWildcard = domain === "*" || domain.startsWith("*.");
+    let isHttpUrl = false;
+    try {
+      const parsed = new URL(domain);
+      isHttpUrl = parsed.protocol === "http:" || parsed.protocol === "https:";
+    } catch {}
+
+    if (!isWildcard && !isHttpUrl) {
+      return toast.error("Use *, *.example.com, or a full URL like https://example.com");
+    }
+
     // basic validation
     if (domains.includes(domain)) {
       return toast.error("Domain already added");
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/ProjectSettings.jsx` around lines 926 - 938, The addDomain
handler currently only checks for duplicates but accepts invalid formats; update
the addDomain function to perform lightweight client-side validation on
newDomain (after trimming) before calling handleUpdate: allow a single "*" or a
leading wildcard like "*.example.com" or a bare hostname/hostname with
subdomains (no http:// or https://, no path, no ports), reject strings that
start with "http://" or "https://" or contain "/" or ":" (except the "*"
pattern) and call toast.error with a clear message for invalid formats; keep the
existing duplicate check (domains.includes(domain)) and only build updated =
[...domains, domain] and call handleUpdate(updated) when validation passes, then
setNewDomain(""). Ensure the validation logic is implemented inside addDomain
and references newDomain, domains, handleUpdate, setNewDomain.
backend/models/Release.js (1)

18-21: Avoid hardcoded publishedBy default in the model.

Persisting a fixed email here makes attribution inflexible. Prefer requiring publishedBy and setting it from authenticated user context during creation.

Suggested change
     publishedBy: {
         type: String,
-        default: 'yashpouranik124@gmail.com'
+        required: true,
+        trim: true
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/models/Release.js` around lines 18 - 21, The Release model currently
hardcodes publishedBy default; remove the default and make the publishedBy
schema field required (i.e., update the publishedBy field in the Release schema
to type: String and required: true) and then ensure the release creation flow
sets publishedBy from the authenticated user context (e.g., in your
createRelease / release creation handler use req.user.email or equivalent to
populate publishedBy before saving). This keeps the model flexible and ensures
attribution comes from the authenticated user rather than a fixed value.
backend/app.js (1)

89-89: Add request throttling/logging on the releases route for consistency.

/api/releases is mounted without the request controls used on neighboring API groups. Add at least a limiter (and optionally logger) to reduce abuse and operational blind spots.

Suggested change
-app.use('/api/releases', releaseRoute);
+app.use('/api/releases', dashboardLimiter, logger, releaseRoute);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app.js` at line 89, The /api/releases route is mounted without the
request controls used elsewhere; add and apply the same rate-limiting (and
optional request-logging) middleware when mounting releaseRoute. Create or
import the existing limiter middleware (e.g., rateLimiter or express-rate-limit
config) and optional requestLogger, then replace or change the mount to use them
with releaseRoute (for example, app.use('/api/releases', rateLimiter,
requestLogger, releaseRoute)) so the releases endpoints receive the same
throttling/logging behavior as neighboring API groups; ensure you import the
middleware and place the mount near the other API group mounts for consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/controllers/project.controller.js`:
- Around line 703-710: The current code only checks that domains is an array but
can still contain non-string or empty entries; update the validation around the
domains variable to ensure every element is a non-empty string (e.g., typeof
entry === "string" && entry.trim().length > 0) before calling
Project.findOneAndUpdate, return res.status(400) with an explanatory error if
validation fails, and pass a sanitized array (trimmed values) into the { $set: {
allowedDomains: domains } } update so Project.findOneAndUpdate (using
req.params.projectId and req.user._id) only persists valid domain strings.

In `@backend/controllers/release.controller.js`:
- Line 5: Replace the hardcoded ADMIN_EMAIL constant and any direct equality
checks against it with a configurable authorization approach: remove the const
ADMIN_EMAIL and instead read an allowlist from an environment variable (e.g.,
process.env.ADMIN_EMAILS split by comma) and/or use role/claim-based checks on
the authenticated user (e.g., req.user.role === 'admin'); update the
authorization logic that currently compares emails (the occurrences that
reference ADMIN_EMAIL) to consult the allowlist and/or the user's role/claims,
and move the check into reusable middleware/helper (e.g., isAdmin(req)) so
rotation and testing are easier.
- Around line 21-33: The release `content` field is stored raw which allows
stored XSS; before constructing and saving the Release (where you reference
req.body.content and call new Release(...) / newRelease.save()), sanitize or
strip unsafe HTML on the server side (e.g., use a well‑maintained sanitizer like
sanitize-html or server DOMPurify) to allow only an approved subset of
tags/attributes or to remove scripts and event handlers, validate the sanitized
result is non-empty, and then save that sanitized content; alternatively ensure
the frontend always escapes/encodes content on render, but the immediate fix is
to sanitize req.body.content prior to new Release(...) and newRelease.save().
- Around line 40-47: The current emails.forEach loop calls
emailQueue.add('release-email', { email, version, title, content }) but doesn't
await the promises, so enqueue failures are missed; change the loop to collect
the promises (e.g., map over emails to call emailQueue.add(...)), await
Promise.all on that array inside the relevant controller function (the block
containing emails.forEach), and handle/rethrow or respond with an error if any
Promise rejects so the API only returns success when all jobs are enqueued.

In `@backend/middleware/verifyApiKey.js`:
- Around line 76-83: The wildcard origin check should compare hostnames, not the
full origin; replace use of originUrl = new URL(origin).origin with extracting
hostname via new URL(origin).hostname and then, in the allowedDomains.some
callback (the logic referencing allowedDomains and baseDomain), treat a
'*.example.com' entry as matching only when hostname === baseDomain or
hostname.endsWith('.' + baseDomain) to enforce the subdomain boundary; leave the
exact-match branch to compare hostname === domain for non-wildcard entries.

In `@backend/models/Project.js`:
- Around line 48-51: The Project schema in backend/models/Project.js is missing
the rateLimit field so per-project overrides read by
backend/middleware/projectRateLimiter.js (req.project.rateLimit) are never
persisted; add a rateLimit property to the Project model (e.g., rateLimit with
the appropriate type expected by projectRateLimiter—Number for a simple max or
an object/Schema.Types.Mixed if the middleware expects window/max pairs—and set
a sensible default/null) so saving a project stores the override and
req.project.rateLimit is populated when loaded.

In `@backend/queues/emailQueue.js`:
- Around line 18-22: Logs in the email queue are printing full recipient
addresses (PII); update the logging in the email processing code that calls
sendReleaseEmail (the console.log and console.error lines that reference the
email variable) to avoid raw emails by either logging a masked version (e.g.,
show only local-part initials and domain) or use the job ID/recipient hash
instead, and apply the same change to the other occurrence referenced around the
second log (line ~37) so no raw email addresses are written to logs.
- Around line 19-23: The worker currently awaits sendReleaseEmail(email, {
version, title, content }) but ignores its returned error object, so provider
failures can be treated as successes; update the handler in emailQueue.js to
inspect the result from sendReleaseEmail (e.g., returned value or { error }
payload), and if an error is present, log the error with context (email,
version) and rethrow or return a rejected promise so the job is marked failed;
ensure any existing try/catch around sendReleaseEmail uses the captured result
(not just thrown exceptions) to decide success vs failure.

In `@backend/utils/emailService.js`:
- Around line 52-57: The current email service logs failures and returns {data,
error} or {error} (the inner callback branch and the outer catch in
backend/utils/emailService.js) which lets callers treat failures as successes;
update both places to log the error and then throw the error (or rethrow a
wrapped Error with context) instead of returning an object—locate the
error-handling in the function that calls the resend client callback (the if
(error) console.error("[Resend Error]", error); return { data, error }; block)
and the enclosing catch block (console.error("[Email Service Error]", error);
return { error };) and replace the returns with throw error (or throw new
Error(`[EmailService] send failed: ${error.message || error}`)) so callers
receive a rejected exception.

In `@frontend/src/pages/ProjectDetails.jsx`:
- Around line 173-175: The two roll buttons share the boolean isRegenerating so
both spin together; change this to track which key is regenerating (e.g., use
state like regeneratingKey: null | 'publishable' | 'secret'), update
handleRegenerateKey(keyType) to set regeneratingKey to the keyType before async
work and clear it on finish/error, and update each button’s disabled and spinner
class logic to check regeneratingKey === 'publishable' or === 'secret' instead
of isRegenerating so only the button for the active key shows loading.

In `@frontend/src/pages/ProjectSettings.jsx`:
- Around line 1045-1063: The icon-only remove button lacks an accessible name;
update the JSX button that calls removeDomain(domain) (the element rendering <X
size={16} />) to include an aria-label (or aria-labelledby) such as
aria-label={`Remove domain ${domain}`} so screen readers get meaningful context;
you can also keep a title attribute for tooltip parity but ensure aria-label is
present and dynamic per domain.

In `@frontend/src/pages/Releases.jsx`:
- Around line 93-96: In Releases.jsx replace the div that uses
dangerouslySetInnerHTML with a plain-text rendering: remove
dangerouslySetInnerHTML and its .replace(/\n/g, '<br>') call and instead render
{release.content} as the div's child, keeping the existing style but add
whiteSpace: 'pre-wrap' (e.g., style={{ color: 'var(--color-text-muted)',
lineHeight: '1.7', fontSize: '1.05rem', whiteSpace: 'pre-wrap' }}). This
preserves newlines safely and eliminates the XSS risk from
dangerouslySetInnerHTML while retaining the visual formatting for the Releases
component.

---

Outside diff comments:
In `@frontend/src/pages/CreateProject.jsx`:
- Line 133: Update the button text in the CreateProject component that currently
reads "I have saved the key, Go to Dashboard" to use plural "keys" for
consistency with the warning copy; locate the JSX button/label in
CreateProject.jsx (the element rendering that exact string) and change it to "I
have saved the keys, Go to Dashboard" (or similar pluralized phrasing).

---

Nitpick comments:
In `@backend/app.js`:
- Line 89: The /api/releases route is mounted without the request controls used
elsewhere; add and apply the same rate-limiting (and optional request-logging)
middleware when mounting releaseRoute. Create or import the existing limiter
middleware (e.g., rateLimiter or express-rate-limit config) and optional
requestLogger, then replace or change the mount to use them with releaseRoute
(for example, app.use('/api/releases', rateLimiter, requestLogger,
releaseRoute)) so the releases endpoints receive the same throttling/logging
behavior as neighboring API groups; ensure you import the middleware and place
the mount near the other API group mounts for consistency.

In `@backend/models/Release.js`:
- Around line 18-21: The Release model currently hardcodes publishedBy default;
remove the default and make the publishedBy schema field required (i.e., update
the publishedBy field in the Release schema to type: String and required: true)
and then ensure the release creation flow sets publishedBy from the
authenticated user context (e.g., in your createRelease / release creation
handler use req.user.email or equivalent to populate publishedBy before saving).
This keeps the model flexible and ensures attribution comes from the
authenticated user rather than a fixed value.

In `@frontend/src/pages/ProjectSettings.jsx`:
- Around line 926-938: The addDomain handler currently only checks for
duplicates but accepts invalid formats; update the addDomain function to perform
lightweight client-side validation on newDomain (after trimming) before calling
handleUpdate: allow a single "*" or a leading wildcard like "*.example.com" or a
bare hostname/hostname with subdomains (no http:// or https://, no path, no
ports), reject strings that start with "http://" or "https://" or contain "/" or
":" (except the "*" pattern) and call toast.error with a clear message for
invalid formats; keep the existing duplicate check (domains.includes(domain))
and only build updated = [...domains, domain] and call handleUpdate(updated)
when validation passes, then setNewDomain(""). Ensure the validation logic is
implemented inside addDomain and references newDomain, domains, handleUpdate,
setNewDomain.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 93850855-87e2-4a61-ad2b-39e59c6fdfff

📥 Commits

Reviewing files that changed from the base of the PR and between 51dcf0a and e2ddf05.

⛔ Files ignored due to path filters (1)
  • backend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (28)
  • backend/app.js
  • backend/controllers/auth.controller.js
  • backend/controllers/project.controller.js
  • backend/controllers/release.controller.js
  • backend/controllers/schema.controller.js
  • backend/middleware/projectRateLimiter.js
  • backend/middleware/requireSecretKey.js
  • backend/middleware/verifyApiKey.js
  • backend/models/Project.js
  • backend/models/Release.js
  • backend/package.json
  • backend/queues/emailQueue.js
  • backend/routes/data.js
  • backend/routes/projects.js
  • backend/routes/releases.js
  • backend/routes/schemas.js
  • backend/routes/storage.js
  • backend/utils/api.js
  • backend/utils/emailService.js
  • frontend/src/App.jsx
  • frontend/src/components/Layout/Sidebar.jsx
  • frontend/src/components/TryItPanel.jsx
  • frontend/src/pages/AdminCreateRelease.jsx
  • frontend/src/pages/CreateProject.jsx
  • frontend/src/pages/ProjectDetails.jsx
  • frontend/src/pages/ProjectSettings.jsx
  • frontend/src/pages/Releases.jsx
  • frontend/src/pages/Signup.jsx

Comment on lines +703 to +710
if (!Array.isArray(domains)) {
return res.status(400).json({ error: "domains must be an array of strings." });
}

const project = await Project.findOneAndUpdate(
{ _id: req.params.projectId, owner: req.user._id },
{ $set: { allowedDomains: domains } },
{ new: true }

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 | 🟠 Major

Validate each domain entry, not only array shape.

At Line 703, the check only confirms domains is an array. Non-string or empty values can still be persisted and later break or weaken origin checks.

🔧 Proposed fix
 module.exports.updateAllowedDomains = async (req, res) => {
     try {
         const { domains } = req.body;
-        if (!Array.isArray(domains)) {
+        if (
+            !Array.isArray(domains) ||
+            domains.some((d) => typeof d !== "string" || !d.trim())
+        ) {
             return res.status(400).json({ error: "domains must be an array of strings." });
         }
+        const normalizedDomains = [...new Set(domains.map((d) => d.trim().toLowerCase()))];

         const project = await Project.findOneAndUpdate(
             { _id: req.params.projectId, owner: req.user._id },
-            { $set: { allowedDomains: domains } },
+            { $set: { allowedDomains: normalizedDomains } },
             { new: true }
         );
📝 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
if (!Array.isArray(domains)) {
return res.status(400).json({ error: "domains must be an array of strings." });
}
const project = await Project.findOneAndUpdate(
{ _id: req.params.projectId, owner: req.user._id },
{ $set: { allowedDomains: domains } },
{ new: true }
if (
!Array.isArray(domains) ||
domains.some((d) => typeof d !== "string" || !d.trim())
) {
return res.status(400).json({ error: "domains must be an array of strings." });
}
const normalizedDomains = [...new Set(domains.map((d) => d.trim().toLowerCase()))];
const project = await Project.findOneAndUpdate(
{ _id: req.params.projectId, owner: req.user._id },
{ $set: { allowedDomains: normalizedDomains } },
{ new: true }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/project.controller.js` around lines 703 - 710, The
current code only checks that domains is an array but can still contain
non-string or empty entries; update the validation around the domains variable
to ensure every element is a non-empty string (e.g., typeof entry === "string"
&& entry.trim().length > 0) before calling Project.findOneAndUpdate, return
res.status(400) with an explanatory error if validation fails, and pass a
sanitized array (trimmed values) into the { $set: { allowedDomains: domains } }
update so Project.findOneAndUpdate (using req.params.projectId and req.user._id)
only persists valid domain strings.

const Developer = require("../models/Developer");
const { emailQueue } = require("../queues/emailQueue");

const ADMIN_EMAIL = 'yashpouranik124@gmail.com';

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 | 🟠 Major

Avoid hardcoding admin identity in controller logic.

Using a single inline email for authorization is brittle and hard to rotate. Move admin authorization to role/claim-based checks or env-configured allowlist.

Also applies to: 24-26

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/release.controller.js` at line 5, Replace the hardcoded
ADMIN_EMAIL constant and any direct equality checks against it with a
configurable authorization approach: remove the const ADMIN_EMAIL and instead
read an allowlist from an environment variable (e.g., process.env.ADMIN_EMAILS
split by comma) and/or use role/claim-based checks on the authenticated user
(e.g., req.user.role === 'admin'); update the authorization logic that currently
compares emails (the occurrences that reference ADMIN_EMAIL) to consult the
allowlist and/or the user's role/claims, and move the check into reusable
middleware/helper (e.g., isAdmin(req)) so rotation and testing are easier.

Comment on lines +21 to +33
const { version, title, content } = req.body;

// Verify Admin
if (req.user.email !== ADMIN_EMAIL) {
return res.status(403).json({ error: "Access denied. Admin only." });
}

if (!version || !title || !content) {
return res.status(400).json({ error: "Missing version, title, or content" });
}

const newRelease = new Release({ version, title, content });
await newRelease.save();

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

Stored XSS risk in release content pipeline.

content is accepted as-is and persisted without sanitization. Since it is later injected as HTML in the frontend, this enables stored script injection.

Suggested change
+const escapeHtml = (value = '') =>
+  value
+    .replace(/&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/"/g, '&quot;')
+    .replace(/'/g, '&#39;');

 exports.createRelease = async (req, res) => {
     try {
-        const { version, title, content } = req.body;
+        const { version, title, content } = req.body;

         // Verify Admin
         if (req.user.email !== ADMIN_EMAIL) {
             return res.status(403).json({ error: "Access denied. Admin only." });
         }

-        if (!version || !title || !content) {
+        const safeVersion = String(version || '').trim();
+        const safeTitle = String(title || '').trim();
+        const safeContent = escapeHtml(String(content || '').trim());
+
+        if (!safeVersion || !safeTitle || !safeContent) {
             return res.status(400).json({ error: "Missing version, title, or content" });
         }

-        const newRelease = new Release({ version, title, content });
+        const newRelease = new Release({
+          version: safeVersion,
+          title: safeTitle,
+          content: safeContent,
+          publishedBy: req.user.email
+        });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/release.controller.js` around lines 21 - 33, The release
`content` field is stored raw which allows stored XSS; before constructing and
saving the Release (where you reference req.body.content and call new
Release(...) / newRelease.save()), sanitize or strip unsafe HTML on the server
side (e.g., use a well‑maintained sanitizer like sanitize-html or server
DOMPurify) to allow only an approved subset of tags/attributes or to remove
scripts and event handlers, validate the sanitized result is non-empty, and then
save that sanitized content; alternatively ensure the frontend always
escapes/encodes content on render, but the immediate fix is to sanitize
req.body.content prior to new Release(...) and newRelease.save().

Comment thread backend/controllers/release.controller.js Outdated
Comment on lines +76 to +83
const originUrl = new URL(origin).origin;
const isAllowed = allowedDomains.some(domain => {
if (domain.startsWith('*.')) {
const baseDomain = domain.substring(2);
return originUrl.endsWith(baseDomain);
}
return originUrl === domain;
});

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

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the verifyApiKey.js file
fd -type f -name "verifyApiKey.js" | head -5

Repository: yash-pouranik/urBackend

Length of output: 237


🏁 Script executed:

# Check lines 76-83 of the file
find . -name "verifyApiKey.js" -type f -exec sed -n '76,83p' {} +

Repository: yash-pouranik/urBackend

Length of output: 492


🏁 Script executed:

node <<'EOF'
// Test URL.origin behavior to confirm the vulnerability

// Test case 1: Legitimate subdomain
const url1 = new URL('https://api.example.com/path');
console.log("url1.origin:", url1.origin);
console.log("url1.hostname:", url1.hostname);

// Test case 2: Sibling domain (evil)
const url2 = new URL('https://evil-example.com/path');
console.log("\nurl2.origin:", url2.origin);
console.log("url2.hostname:", url2.hostname);

// Test the vulnerable endsWith check
const baseDomain = 'example.com';
console.log("\nVulnerable check (originUrl.endsWith):");
console.log("url1.origin.endsWith('" + baseDomain + "'):", url1.origin.endsWith(baseDomain));
console.log("url2.origin.endsWith('" + baseDomain + "'):", url2.origin.endsWith(baseDomain));

// Test the proposed fix (hostname-based)
console.log("\nProposed fix (hostname-based):");
const isAllowedUrl1 = url1.hostname === baseDomain || url1.hostname.endsWith('.' + baseDomain);
const isAllowedUrl2 = url2.hostname === baseDomain || url2.hostname.endsWith('.' + baseDomain);
console.log("url1 allowed:", isAllowedUrl1);
console.log("url2 allowed:", isAllowedUrl2);
EOF

Repository: yash-pouranik/urBackend

Length of output: 400


Wildcard origin matching bypasses subdomain boundary validation.

Line 80 uses originUrl.endsWith(baseDomain) which allows https://evil-example.com to pass when *.example.com is allowed. The .origin property includes the protocol, so suffix matching on the full origin string does not establish a proper subdomain boundary. For *.example.com, both https://api.example.com and https://evil-example.com satisfy .endsWith('example.com').

Extract the hostname separately and check for exact match or subdomain boundary with a leading dot:

Proposed fix
-                    const originUrl = new URL(origin).origin; 
+                    const parsedOrigin = new URL(origin);
+                    const originUrl = parsedOrigin.origin;
+                    const originHost = parsedOrigin.hostname.toLowerCase();

                     const isAllowed = allowedDomains.some(domain => {
                         if (domain.startsWith('*.')) {
-                            const baseDomain = domain.substring(2);
-                            return originUrl.endsWith(baseDomain);
+                            const baseDomain = domain.substring(2).toLowerCase();
+                            return originHost === baseDomain || originHost.endsWith(`.${baseDomain}`);
                         }
                         return originUrl === domain;
                     });
📝 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
const originUrl = new URL(origin).origin;
const isAllowed = allowedDomains.some(domain => {
if (domain.startsWith('*.')) {
const baseDomain = domain.substring(2);
return originUrl.endsWith(baseDomain);
}
return originUrl === domain;
});
const parsedOrigin = new URL(origin);
const originUrl = parsedOrigin.origin;
const originHost = parsedOrigin.hostname.toLowerCase();
const isAllowed = allowedDomains.some(domain => {
if (domain.startsWith('*.')) {
const baseDomain = domain.substring(2).toLowerCase();
return originHost === baseDomain || originHost.endsWith(`.${baseDomain}`);
}
return originUrl === domain;
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/middleware/verifyApiKey.js` around lines 76 - 83, The wildcard origin
check should compare hostnames, not the full origin; replace use of originUrl =
new URL(origin).origin with extracting hostname via new URL(origin).hostname and
then, in the allowedDomains.some callback (the logic referencing allowedDomains
and baseDomain), treat a '*.example.com' entry as matching only when hostname
=== baseDomain or hostname.endsWith('.' + baseDomain) to enforce the subdomain
boundary; leave the exact-match branch to compare hostname === domain for
non-wildcard entries.

Comment on lines +19 to +23
await sendReleaseEmail(email, { version, title, content });
} catch (error) {
console.error(`[Queue] Failed to send email to ${email}:`, error);
throw error;
}

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

Provider send failures can be falsely marked as successful jobs.

sendReleaseEmail returns an error object on provider failure; this worker does not check it, so failed sends can be treated as completed jobs.

Suggested change
-        await sendReleaseEmail(email, { version, title, content });
+        const result = await sendReleaseEmail(email, { version, title, content });
+        if (result?.error) {
+            throw new Error(`Release email send failed for ${email}`);
+        }
📝 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
await sendReleaseEmail(email, { version, title, content });
} catch (error) {
console.error(`[Queue] Failed to send email to ${email}:`, error);
throw error;
}
const result = await sendReleaseEmail(email, { version, title, content });
if (result?.error) {
throw new Error(`Release email send failed for ${email}`);
}
} catch (error) {
console.error(`[Queue] Failed to send email to ${email}:`, error);
throw error;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/queues/emailQueue.js` around lines 19 - 23, The worker currently
awaits sendReleaseEmail(email, { version, title, content }) but ignores its
returned error object, so provider failures can be treated as successes; update
the handler in emailQueue.js to inspect the result from sendReleaseEmail (e.g.,
returned value or { error } payload), and if an error is present, log the error
with context (email, version) and rethrow or return a rejected promise so the
job is marked failed; ensure any existing try/catch around sendReleaseEmail uses
the captured result (not just thrown exceptions) to decide success vs failure.

Comment thread backend/utils/emailService.js Outdated
Comment thread frontend/src/pages/ProjectDetails.jsx Outdated
Comment thread frontend/src/pages/ProjectSettings.jsx
Comment thread frontend/src/pages/Releases.jsx Outdated

@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.

♻️ Duplicate comments (1)
backend/routes/releases.js (1)

16-16: ⚠️ Potential issue | 🟠 Major

Add rate limiting to POST / to protect write + email-queue side effects.

Line 16 is authenticated but unthrottled. This leaves release creation and downstream email enqueuing vulnerable to burst abuse or accidental flooding.

🔧 Proposed patch
 const getAllReleasesLimiter = RateLimit({
   windowMs: 15 * 60 * 1000,
   max: 1000, 
 });
+
+const createReleaseLimiter = RateLimit({
+  windowMs: 15 * 60 * 1000,
+  max: 10,
+});
@@
-router.post('/', authorization, createRelease);
+router.post('/', createReleaseLimiter, authorization, createRelease);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/routes/releases.js` at line 16, The POST / route is authenticated but
unthrottled (router.post('/', authorization, createRelease)); add a
rate-limiting middleware to protect the createRelease handler and downstream
email-queue side effects by importing or creating an express rate limiter (e.g.
express-rate-limit or existing shared limiter), configuring sensible window and
max values (e.g. short window for per-IP/user write ops), and inserting that
middleware into the route chain (e.g. router.post('/', authorization,
rateLimiter, createRelease) or router.post('/', rateLimiter, authorization,
createRelease) depending on whether you want to limit unauthenticated attempts)
so bursts are throttled while legitimate users still can create releases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@backend/routes/releases.js`:
- Line 16: The POST / route is authenticated but unthrottled (router.post('/',
authorization, createRelease)); add a rate-limiting middleware to protect the
createRelease handler and downstream email-queue side effects by importing or
creating an express rate limiter (e.g. express-rate-limit or existing shared
limiter), configuring sensible window and max values (e.g. short window for
per-IP/user write ops), and inserting that middleware into the route chain (e.g.
router.post('/', authorization, rateLimiter, createRelease) or router.post('/',
rateLimiter, authorization, createRelease) depending on whether you want to
limit unauthenticated attempts) so bursts are throttled while legitimate users
still can create releases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 90f4bf17-224d-48a1-abc3-7062c144477a

📥 Commits

Reviewing files that changed from the base of the PR and between e2ddf05 and 2caebbe.

📒 Files selected for processing (1)
  • backend/routes/releases.js

@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: 4

♻️ Duplicate comments (2)
backend/controllers/release.controller.js (2)

21-37: ⚠️ Potential issue | 🔴 Critical

Sanitize content before saving to prevent stored XSS.

At Line 21 and Line 35, user-controlled content is stored raw. If rendered as HTML later, this is a stored XSS path.

Suggested fix (server-side sanitization)
+const sanitizeHtml = require("sanitize-html");
@@
-        const { version, title, content } = req.body;
+        const safeVersion = String(req.body.version || "").trim();
+        const safeTitle = String(req.body.title || "").trim();
+        const safeContent = sanitizeHtml(String(req.body.content || ""), {
+            allowedTags: ["p", "ul", "ol", "li", "strong", "em", "a", "code", "pre", "br"],
+            allowedAttributes: { a: ["href", "target", "rel"] }
+        }).trim();
@@
-        if (!version || !title || !content) {
+        if (!safeVersion || !safeTitle || !safeContent) {
             return res.status(400).json({ error: "Missing version, title, or content" });
         }
@@
-            version, 
-            title, 
-            content,
+            version: safeVersion, 
+            title: safeTitle, 
+            content: safeContent,
             publishedBy: req.user.email
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/release.controller.js` around lines 21 - 37, Sanitize the
user-controlled content before saving to prevent stored XSS: validate and
replace req.body.content with a server-side sanitized version (e.g., using a
library like sanitize-html or DOMPurify for Node) before you construct
newRelease; update the code around the extraction const { version, title,
content } = req.body to sanitize the content variable and pass the sanitized
value into new Release({ version, title, content: sanitizedContent, publishedBy:
req.user.email }); also add the required import/dependency and unit tests to
ensure dangerous HTML/scripts are stripped or escaped.

45-57: ⚠️ Potential issue | 🟠 Major

Avoid fail-fast enqueue after persistence; handle partial queue results explicitly.

At Line 45, Promise.all rejects on first failure, but some jobs may already be queued and the release is already saved. This creates partial-success ambiguity and retry duplication risk.

Suggested resilient enqueue handling
-        await Promise.all(emails.map(email => 
-            emailQueue.add('release-email', {
-                email,
-                version,
-                title,
-                content
-            })
-        ));
+        const enqueueResults = await Promise.allSettled(
+            emails.map((email) =>
+                emailQueue.add("release-email", {
+                    email,
+                    version: newRelease.version,
+                    title: newRelease.title,
+                    content: newRelease.content,
+                })
+            )
+        );
+
+        const queued = enqueueResults.filter((r) => r.status === "fulfilled").length;
+        const failed = enqueueResults.length - queued;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/release.controller.js` around lines 45 - 57, The current
use of Promise.all over emails.map with emailQueue.add can reject on the first
enqueue error and obscure that the release was persisted and some jobs were
queued; change to a resilient enqueue loop that collects per-email results
(success/failure) by awaiting each emailQueue.add inside a try/catch (or using
Promise.allSettled on the emails.map) so you can record failures without
throwing, then respond with res.status(201).json including the total attempted,
succeeded, and failed counts and a list or summary of failed email addresses;
update code paths around Promise.all, emails.map, and emailQueue.add to return
partial-results and avoid duplicative retries.
🧹 Nitpick comments (6)
backend/models/Release.js (1)

14-21: Add normalization and size guards for persisted release fields.

content and publishedBy are required, but currently unbounded/untrimmed. This can allow oversized payloads and inconsistent author values in storage.

Proposed schema hardening
 const ReleaseSchema = new mongoose.Schema({
     version: {
         type: String,
         required: true,
-        trim: true
+        trim: true,
+        maxlength: 64
     },
     title: {
         type: String,
         required: true,
-        trim: true
+        trim: true,
+        maxlength: 200
     },
     content: {
         type: String,
-        required: true
+        required: true,
+        trim: true,
+        maxlength: 20000
     },
     publishedBy: {
         type: String,
-        required: true
+        required: true,
+        trim: true,
+        lowercase: true
     }
 }, { timestamps: true });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/models/Release.js` around lines 14 - 21, The Release model's required
fields 'content' and 'publishedBy' lack normalization and size limits; update
the schema in Release.js to add trim and maxlength constraints (e.g., set a
reasonable max for 'content' like 10000 chars and for 'publishedBy' like 100 or
as your domain requires) and normalize 'publishedBy' (trim and optionally
lowercase) to prevent oversized payloads and inconsistent author values; ensure
validation messages remain descriptive so Mongoose rejects invalid documents.
backend/controllers/project.controller.js (1)

700-729: Domain validation improved but could deduplicate entries.

The validation now correctly checks that all entries are strings (line 703) and filters empty values (lines 707-709). This addresses the previous review concern. However, consider adding deduplication to prevent storing redundant entries:

Optional enhancement
 const cleanedDomains = domains
     .map(d => d.trim())
-    .filter(d => d.length > 0);
+    .filter(d => d.length > 0)
+    .filter((d, i, arr) => arr.indexOf(d) === i); // deduplicate
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/project.controller.js` around lines 700 - 729, The
updateAllowedDomains handler currently trims and filters domains into
cleanedDomains but doesn't remove duplicates; update the cleanedDomains creation
inside updateAllowedDomains to deduplicate values (e.g., use a Set or
Array.prototype.filter with indexOf) so only unique, trimmed, non-empty strings
are stored, then continue to persist via Project.findOneAndUpdate and update
caches via deleteProjectById/setProjectById and deleteProjectByApiKeyCache as
before.
backend/middleware/verifyApiKey.js (1)

68-68: Consider removing referer fallback for origin validation.

Using req.headers.referer as a fallback introduces inconsistency since Referer can be stripped by browser policies or proxies, and has a different URL format than Origin. The Origin header is the canonical header for CORS validation. If Origin is missing, the browser is likely not making a cross-origin request.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/middleware/verifyApiKey.js` at line 68, The origin validation
currently falls back to req.headers.referer which is inconsistent with CORS
semantics; change the code in verifyApiKey middleware to use only
req.headers.origin (i.e., const origin = req.headers.origin) and remove any
logic that treats referer as an equivalent fallback, updating any subsequent
checks or error messages that reference referer so they assume Origin may be
undefined for same-origin requests.
frontend/src/pages/ProjectSettings.jsx (1)

926-938: Consider adding domain format validation.

The addDomain function only checks for empty strings and duplicates. Invalid entries like not a domain or http:// could be saved and later cause issues in the backend's origin matching. Consider basic format validation:

Suggested validation
 const addDomain = () => {
     const domain = newDomain.trim();
     if (!domain) return;
     
+    // Basic validation for wildcard or URL-like format
+    const isWildcard = domain === '*' || domain.startsWith('*.');
+    const isValidUrl = /^https?:\/\/[\w.-]+(:\d+)?$/.test(domain);
+    if (!isWildcard && !isValidUrl) {
+        return toast.error("Enter a valid domain (e.g., https://example.com or *.example.com)");
+    }
+    
     if (domains.includes(domain)) {
         return toast.error("Domain already added");
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/ProjectSettings.jsx` around lines 926 - 938, The addDomain
function currently only trims and checks duplicates; add a format check before
pushing to domains by validating newDomain (e.g., no spaces or URL schemes, must
be a valid hostname with optional port). In the addDomain flow (using newDomain,
domains, handleUpdate, setNewDomain), run a simple validation (for example a
regex that allows letters/numbers, dashes, dots and optional :port and requires
at least one dot, or use URL parsing to reject strings with protocols or
slashes) and if invalid call toast.error("Invalid domain format") and return;
only then build updated and call handleUpdate and setNewDomain. Ensure the
validation rejects entries like "not a domain" or "http://example.com" while
allowing "example.com" and "sub.example.com:8080".
frontend/src/pages/Releases.jsx (2)

18-30: Consider adding request cancellation on unmount.

If the component unmounts before the fetch completes, setReleases and setLoading will be called on an unmounted component. While React 18+ handles this gracefully, adding an AbortController is a cleaner pattern.

♻️ Suggested improvement
     useEffect(() => {
+        const controller = new AbortController();
         const fetchReleases = async () => {
             try {
-                const res = await axios.get(`${API_URL}/api/releases`);
+                const res = await axios.get(`${API_URL}/api/releases`, {
+                    signal: controller.signal
+                });
                 setReleases(res.data);
             } catch (err) {
-                console.error("Failed to fetch releases", err);
+                if (!axios.isCancel(err)) {
+                    console.error("Failed to fetch releases", err);
+                }
             } finally {
                 setLoading(false);
             }
         };
         fetchReleases();
+        return () => controller.abort();
     }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/Releases.jsx` around lines 18 - 30, The fetch in the
useEffect (function fetchReleases) should be cancellable: create an
AbortController, pass its signal into the axios.get call (or an axios
CancelToken if your axios version requires), and in the cleanup return () =>
controller.abort(); also guard calls to setReleases and setLoading by checking
for controller.signal.aborted (or catch the AbortError) so you don't call
setReleases/setLoading after unmount; update references to useEffect,
fetchReleases, setReleases, setLoading and the axios.get invocation accordingly.

123-128: Inconsistent use of hardcoded colors vs CSS variables.

The styles mix CSS variables (e.g., var(--color-primary)) with hardcoded colors (#fff, #1e1e1e, rgba(255, 255, 255, ...)). This inconsistency may cause issues if theming is added later.

Consider replacing hardcoded values with CSS variables for consistency:

  • #fffvar(--color-text) or similar
  • #1e1e1evar(--color-bg-tertiary) or similar
  • rgba(255, 255, 255, ...) → appropriate CSS variable with opacity

Also applies to: 154-156, 173-173

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/Releases.jsx` around lines 123 - 128, Replace hardcoded
colors in the markdown styles with CSS variables to ensure consistent theming:
update selectors like .markdown-content h1, .markdown-content h2,
.markdown-content h3 and other occurrences at the noted ranges (e.g., the rules
around lines 154-156 and 173) to use variables such as var(--color-text) instead
of `#fff`, var(--color-bg-tertiary) instead of `#1e1e1e`, and use rgba() via a CSS
variable with alpha (e.g., rgba(var(--color-text-rgb), 0.8)) or a dedicated
translucent variable instead of hardcoded rgba(...) so all themeable colors come
from CSS variables.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/controllers/release.controller.js`:
- Around line 5-6: The module reads ADMIN_EMAIL into the constant ADMIN_EMAIL
but does not fail fast if it's undefined; add an explicit guard immediately
after the const ADMIN_EMAIL = process.env.ADMIN_EMAIL; statement that checks for
a falsy value and throws a clear Error (e.g., throw new Error('ADMIN_EMAIL
environment variable is required')) so the app fails to start deterministically
when ADMIN_EMAIL is not configured; ensure the check runs at module
initialization (not inside a route) so functions that rely on ADMIN_EMAIL (e.g.,
any auth logic in this controller) never run with a missing value.

In `@backend/utils/emailService.js`:
- Around line 63-70: The escapeHtml function can throw when passed
null/undefined or non-string values; make it null-safe by coercing its input to
a string up front (e.g., use String(unsafe) or a typeof check) before performing
the chained .replace calls so sendReleaseEmail and other callers won't crash on
malformed payloads; update the escapeHtml helper accordingly and ensure callers
still pass values but rely on the helper to defensively handle non-strings.
- Line 6: The current initialization of the resend client silently falls back to
a dummy key; update the logic around the Resend constructor (the resend
variable) to fail fast when no real key is provided: remove the
're_dummy_key_for_testing' fallback and instead read
process.env.RESEND_API_KEY_2 || process.env.RESEND_API_KEY, and if that value is
missing and NODE_ENV !== 'test' throw a descriptive Error (or exit) so missing
production config is detected at startup; allow the dummy/test key only when
NODE_ENV === 'test' (or when an explicit test flag is set).

In `@frontend/src/config.js`:
- Line 3: Replace the hardcoded ADMIN_EMAIL constant by reading it from an
environment-config mechanism so the frontend and backend use the same source of
truth; change the export for ADMIN_EMAIL in config.js to derive its value from
process.env (or your app's runtime config provider) with a safe fallback only
for local dev, and ensure the frontend build system/documentation instructs
setting the same ADMIN_EMAIL env var used by backend (reference symbol:
ADMIN_EMAIL in config.js and the backend check in release.controller.js) to
avoid divergence and remove PII from source.

---

Duplicate comments:
In `@backend/controllers/release.controller.js`:
- Around line 21-37: Sanitize the user-controlled content before saving to
prevent stored XSS: validate and replace req.body.content with a server-side
sanitized version (e.g., using a library like sanitize-html or DOMPurify for
Node) before you construct newRelease; update the code around the extraction
const { version, title, content } = req.body to sanitize the content variable
and pass the sanitized value into new Release({ version, title, content:
sanitizedContent, publishedBy: req.user.email }); also add the required
import/dependency and unit tests to ensure dangerous HTML/scripts are stripped
or escaped.
- Around line 45-57: The current use of Promise.all over emails.map with
emailQueue.add can reject on the first enqueue error and obscure that the
release was persisted and some jobs were queued; change to a resilient enqueue
loop that collects per-email results (success/failure) by awaiting each
emailQueue.add inside a try/catch (or using Promise.allSettled on the
emails.map) so you can record failures without throwing, then respond with
res.status(201).json including the total attempted, succeeded, and failed counts
and a list or summary of failed email addresses; update code paths around
Promise.all, emails.map, and emailQueue.add to return partial-results and avoid
duplicative retries.

---

Nitpick comments:
In `@backend/controllers/project.controller.js`:
- Around line 700-729: The updateAllowedDomains handler currently trims and
filters domains into cleanedDomains but doesn't remove duplicates; update the
cleanedDomains creation inside updateAllowedDomains to deduplicate values (e.g.,
use a Set or Array.prototype.filter with indexOf) so only unique, trimmed,
non-empty strings are stored, then continue to persist via
Project.findOneAndUpdate and update caches via deleteProjectById/setProjectById
and deleteProjectByApiKeyCache as before.

In `@backend/middleware/verifyApiKey.js`:
- Line 68: The origin validation currently falls back to req.headers.referer
which is inconsistent with CORS semantics; change the code in verifyApiKey
middleware to use only req.headers.origin (i.e., const origin =
req.headers.origin) and remove any logic that treats referer as an equivalent
fallback, updating any subsequent checks or error messages that reference
referer so they assume Origin may be undefined for same-origin requests.

In `@backend/models/Release.js`:
- Around line 14-21: The Release model's required fields 'content' and
'publishedBy' lack normalization and size limits; update the schema in
Release.js to add trim and maxlength constraints (e.g., set a reasonable max for
'content' like 10000 chars and for 'publishedBy' like 100 or as your domain
requires) and normalize 'publishedBy' (trim and optionally lowercase) to prevent
oversized payloads and inconsistent author values; ensure validation messages
remain descriptive so Mongoose rejects invalid documents.

In `@frontend/src/pages/ProjectSettings.jsx`:
- Around line 926-938: The addDomain function currently only trims and checks
duplicates; add a format check before pushing to domains by validating newDomain
(e.g., no spaces or URL schemes, must be a valid hostname with optional port).
In the addDomain flow (using newDomain, domains, handleUpdate, setNewDomain),
run a simple validation (for example a regex that allows letters/numbers,
dashes, dots and optional :port and requires at least one dot, or use URL
parsing to reject strings with protocols or slashes) and if invalid call
toast.error("Invalid domain format") and return; only then build updated and
call handleUpdate and setNewDomain. Ensure the validation rejects entries like
"not a domain" or "http://example.com" while allowing "example.com" and
"sub.example.com:8080".

In `@frontend/src/pages/Releases.jsx`:
- Around line 18-30: The fetch in the useEffect (function fetchReleases) should
be cancellable: create an AbortController, pass its signal into the axios.get
call (or an axios CancelToken if your axios version requires), and in the
cleanup return () => controller.abort(); also guard calls to setReleases and
setLoading by checking for controller.signal.aborted (or catch the AbortError)
so you don't call setReleases/setLoading after unmount; update references to
useEffect, fetchReleases, setReleases, setLoading and the axios.get invocation
accordingly.
- Around line 123-128: Replace hardcoded colors in the markdown styles with CSS
variables to ensure consistent theming: update selectors like .markdown-content
h1, .markdown-content h2, .markdown-content h3 and other occurrences at the
noted ranges (e.g., the rules around lines 154-156 and 173) to use variables
such as var(--color-text) instead of `#fff`, var(--color-bg-tertiary) instead of
`#1e1e1e`, and use rgba() via a CSS variable with alpha (e.g.,
rgba(var(--color-text-rgb), 0.8)) or a dedicated translucent variable instead of
hardcoded rgba(...) so all themeable colors come from CSS variables.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 42992233-0167-4e7d-b6a7-cd48402709bd

📥 Commits

Reviewing files that changed from the base of the PR and between 2caebbe and 21e354d.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • backend/controllers/project.controller.js
  • backend/controllers/release.controller.js
  • backend/middleware/verifyApiKey.js
  • backend/models/Release.js
  • backend/package.json
  • backend/tests/jest.setup.js
  • backend/utils/emailService.js
  • frontend/package.json
  • frontend/src/components/Layout/Footer.jsx
  • frontend/src/config.js
  • frontend/src/pages/AdminCreateRelease.jsx
  • frontend/src/pages/ProjectDetails.jsx
  • frontend/src/pages/ProjectSettings.jsx
  • frontend/src/pages/Releases.jsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/package.json

Comment on lines +5 to +6
const ADMIN_EMAIL = process.env.ADMIN_EMAIL;

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 | 🟠 Major

Fail fast when ADMIN_EMAIL is not configured.

If ADMIN_EMAIL is missing, auth behavior becomes config-dependent and brittle. Add an explicit guard before Line 24 so misconfiguration is surfaced deterministically.

Suggested guard
 const ADMIN_EMAIL = process.env.ADMIN_EMAIL;
 
 // CREATE RELEASE (Admin Only)
 exports.createRelease = async (req, res) => {
     try {
+        if (!ADMIN_EMAIL) {
+            return res.status(500).json({ error: "Server misconfiguration: ADMIN_EMAIL is not set" });
+        }
         const { version, title, content } = req.body;

Also applies to: 24-26

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/release.controller.js` around lines 5 - 6, The module
reads ADMIN_EMAIL into the constant ADMIN_EMAIL but does not fail fast if it's
undefined; add an explicit guard immediately after the const ADMIN_EMAIL =
process.env.ADMIN_EMAIL; statement that checks for a falsy value and throws a
clear Error (e.g., throw new Error('ADMIN_EMAIL environment variable is
required')) so the app fails to start deterministically when ADMIN_EMAIL is not
configured; ensure the check runs at module initialization (not inside a route)
so functions that rely on ADMIN_EMAIL (e.g., any auth logic in this controller)
never run with a missing value.


dotenv.config();
const resend = new Resend(process.env.RESEND_API_KEY || 're_dummy_key_for_testing');
const resend = new Resend(process.env.RESEND_API_KEY_2 || process.env.RESEND_API_KEY || 're_dummy_key_for_testing');

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 | 🟠 Major

Do not silently fall back to a dummy Resend key outside tests.

Line 6 masks missing production config and can make email failures show up only at send time. Prefer fail-fast initialization unless explicitly in test mode.

Suggested initialization hardening
-dotenv.config();
-const resend = new Resend(process.env.RESEND_API_KEY_2 || process.env.RESEND_API_KEY || 're_dummy_key_for_testing');
+dotenv.config();
+const resendApiKey = process.env.RESEND_API_KEY_2 || process.env.RESEND_API_KEY;
+if (!resendApiKey && process.env.NODE_ENV !== "test") {
+    throw new Error("Missing RESEND_API_KEY_2 / RESEND_API_KEY");
+}
+const resend = new Resend(resendApiKey || "re_dummy_key_for_testing");
📝 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
const resend = new Resend(process.env.RESEND_API_KEY_2 || process.env.RESEND_API_KEY || 're_dummy_key_for_testing');
const resendApiKey = process.env.RESEND_API_KEY_2 || process.env.RESEND_API_KEY;
if (!resendApiKey && process.env.NODE_ENV !== "test") {
throw new Error("Missing RESEND_API_KEY_2 / RESEND_API_KEY");
}
const resend = new Resend(resendApiKey || "re_dummy_key_for_testing");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/utils/emailService.js` at line 6, The current initialization of the
resend client silently falls back to a dummy key; update the logic around the
Resend constructor (the resend variable) to fail fast when no real key is
provided: remove the 're_dummy_key_for_testing' fallback and instead read
process.env.RESEND_API_KEY_2 || process.env.RESEND_API_KEY, and if that value is
missing and NODE_ENV !== 'test' throw a descriptive Error (or exit) so missing
production config is detected at startup; allow the dummy/test key only when
NODE_ENV === 'test' (or when an explicit test flag is set).

Comment on lines +63 to +70
const escapeHtml = (unsafe) => {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
};

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 | 🟡 Minor

Make escapeHtml null-safe to avoid runtime crashes on malformed payloads.

At Line 64, calling .replace on non-string values throws. Coerce first to keep sendReleaseEmail defensive.

Suggested defensive helper
-const escapeHtml = (unsafe) => {
-    return unsafe
+const escapeHtml = (unsafe = "") => {
+    return String(unsafe)
         .replace(/&/g, "&amp;")
         .replace(/</g, "&lt;")
         .replace(/>/g, "&gt;")
         .replace(/"/g, "&quot;")
         .replace(/'/g, "&#039;");
 };
📝 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
const escapeHtml = (unsafe) => {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
};
const escapeHtml = (unsafe = "") => {
return String(unsafe)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/utils/emailService.js` around lines 63 - 70, The escapeHtml function
can throw when passed null/undefined or non-string values; make it null-safe by
coercing its input to a string up front (e.g., use String(unsafe) or a typeof
check) before performing the chained .replace calls so sendReleaseEmail and
other callers won't crash on malformed payloads; update the escapeHtml helper
accordingly and ensure callers still pass values but rely on the helper to
defensively handle non-strings.

Comment thread frontend/src/config.js
// frontend/src/config.js
export const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:1234'; No newline at end of file
export const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:1234';
export const ADMIN_EMAIL = 'yashpouranik124@gmail.com'; No newline at end of file

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 | 🟠 Major

Hardcoded admin email creates configuration drift risk with backend.

The frontend hardcodes ADMIN_EMAIL while the backend reads from process.env.ADMIN_EMAIL (see backend/controllers/release.controller.js line 5). If these values diverge, a user could pass the frontend guard but fail the backend check, or vice versa. Additionally, hardcoding PII in source code is a compliance concern.

Consider using an environment variable:

-export const ADMIN_EMAIL = 'yashpouranik124@gmail.com';
+export const ADMIN_EMAIL = import.meta.env.VITE_ADMIN_EMAIL;
📝 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
export const ADMIN_EMAIL = 'yashpouranik124@gmail.com';
export const ADMIN_EMAIL = import.meta.env.VITE_ADMIN_EMAIL;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/config.js` at line 3, Replace the hardcoded ADMIN_EMAIL constant
by reading it from an environment-config mechanism so the frontend and backend
use the same source of truth; change the export for ADMIN_EMAIL in config.js to
derive its value from process.env (or your app's runtime config provider) with a
safe fallback only for local dev, and ensure the frontend build
system/documentation instructs setting the same ADMIN_EMAIL env var used by
backend (reference symbol: ADMIN_EMAIL in config.js and the backend check in
release.controller.js) to avoid divergence and remove PII from source.

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app.js`:
- Line 92: The releases routes are mounted without authentication
(app.use('/api/releases', releaseRoute)), so createRelease in
release.controller.js assumes req.user exists and throws; update the routes in
releases.js to apply the imported auth/authorization middleware to the POST '/'
endpoint (or to the router globally) so that req.user is populated before
createRelease runs; ensure the middleware symbol used (e.g., authMiddleware or
authorization) is the same one imported in releases.js and that the POST route
points to the createRelease controller.
- Around line 19-21: The app currently requires('./queues/emailQueue') at
startup which creates an IORedis connection synchronously and can throw if Redis
is down; change the initialization so the Redis client in emailQueue.js is
created with proper error handling or lazily initialized: add a top-level
.on('error', handler) and .on('connect'/'ready') handlers to the IORedis client
inside emailQueue.js, and wrap the initial connection attempt in a try/catch or
export an async init function (e.g., initEmailQueue) that backend/app.js can
call after the Express server binds, falling back to degraded behavior if the
connection fails; ensure any unhandled errors are caught and logged via your
logger instead of letting the require crash the process.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f109f628-e205-4c5f-84ae-5438438688db

📥 Commits

Reviewing files that changed from the base of the PR and between 21e354d and 6b23ae0.

📒 Files selected for processing (2)
  • backend/app.js
  • backend/routes/releases.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/routes/releases.js

Comment thread backend/app.js
Comment on lines +19 to +21
// Initialize Queue Workers
require('./queues/emailQueue');

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 | 🟠 Major

Missing error handling for Redis connection at startup.

The synchronous require('./queues/emailQueue') initializes an IORedis connection immediately. If Redis is unavailable when the server starts, the connection will throw synchronously and crash the Express process before it can bind to a port.

Consider adding connection error handlers or lazy initialization to allow the app to start gracefully and degrade when Redis is down.

🛡️ Suggested approach: wrap initialization with error handling
 // Initialize Queue Workers
-require('./queues/emailQueue');
+try {
+    require('./queues/emailQueue');
+} catch (err) {
+    console.error('❌ Failed to initialize email queue:', err.message);
+    // Optionally: set a flag to disable email features
+}

Alternatively, add .on('error') handlers in emailQueue.js to the IORedis connection to prevent unhandled exceptions.

📝 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
// Initialize Queue Workers
require('./queues/emailQueue');
// Initialize Queue Workers
try {
require('./queues/emailQueue');
} catch (err) {
console.error('❌ Failed to initialize email queue:', err.message);
// Optionally: set a flag to disable email features
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app.js` around lines 19 - 21, The app currently
requires('./queues/emailQueue') at startup which creates an IORedis connection
synchronously and can throw if Redis is down; change the initialization so the
Redis client in emailQueue.js is created with proper error handling or lazily
initialized: add a top-level .on('error', handler) and .on('connect'/'ready')
handlers to the IORedis client inside emailQueue.js, and wrap the initial
connection attempt in a try/catch or export an async init function (e.g.,
initEmailQueue) that backend/app.js can call after the Express server binds,
falling back to degraded behavior if the connection fails; ensure any unhandled
errors are caught and logged via your logger instead of letting the require
crash the process.

Comment thread backend/app.js
app.use('/api/data', limiter, cors(adminCorsOptions), logger, dataRoute);
app.use('/api/schemas', limiter, cors(adminCorsOptions), logger, schemaRoute);
app.use('/api/storage', limiter, cors(adminCorsOptions), logger, storageRoute);
app.use('/api/releases', releaseRoute);

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

Critical: Missing authentication middleware on release routes causes runtime error and security bypass.

The /api/releases route is mounted without authentication middleware. According to backend/routes/releases.js, the authMiddleware is imported but never applied to the POST / endpoint. The createRelease controller (line 24 in release.controller.js) assumes req.user is populated:

if (req.user.email !== ADMIN_EMAIL)

Without auth middleware, req.user is undefined, causing a TypeError: Cannot read property 'email' of undefined instead of a proper 401 response. Additionally, unauthenticated requests can reach the controller logic.

🔒 Proposed fix: Apply auth middleware in the route file

In backend/routes/releases.js, apply the imported authorization middleware to the POST route:

 // CREATE RELEASE (Admin Only)
-router.post('/', createReleaseLimiter, createRelease);
+router.post('/', createReleaseLimiter, authorization, createRelease);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app.js` at line 92, The releases routes are mounted without
authentication (app.use('/api/releases', releaseRoute)), so createRelease in
release.controller.js assumes req.user exists and throws; update the routes in
releases.js to apply the imported auth/authorization middleware to the POST '/'
endpoint (or to the router globally) so that req.user is populated before
createRelease runs; ensure the middleware symbol used (e.g., authMiddleware or
authorization) is the same one imported in releases.js and that the POST route
points to the createRelease controller.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
frontend/src/pages/LandingPage/index.jsx (3)

385-386: ⚠️ Potential issue | 🟡 Minor

Likely typo: "supabase buckets or Supabase Storage" is redundant.

This appears to be a copy error. Based on context (BYO Storage card), this should probably read "S3 buckets or Supabase Storage".

Proposed fix
-                            Link your supabase buckets or Supabase Storage. We handle the file upload tokens, permissions, and CDN delivery automatically.
+                            Link your S3 buckets or Supabase Storage. We handle the file upload tokens, permissions, and CDN delivery automatically.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/LandingPage/index.jsx` around lines 385 - 386, In the
LandingPage component update the paragraph text that currently reads "Link your
supabase buckets or Supabase Storage..." to use the intended storage providers
string (e.g., "Link your S3 buckets or Supabase Storage..."); locate the JSX
paragraph in frontend/src/pages/LandingPage/index.jsx (the <p> element with
inline styles around line 385) and replace the redundant "supabase buckets"
phrasing with "S3 buckets" so the copy correctly reflects BYO Storage options.

122-122: ⚠️ Potential issue | 🟡 Minor

window.innerWidth in render won't respond to viewport changes.

The inline style using window.innerWidth is evaluated only once at render time. Resizing the browser window won't toggle visibility of these elements.

Consider using CSS media queries (already defined in style.css for .nav-links and .mobile-menu-btn) or a resize listener with state.

Proposed fix using CSS classes
-<div className="nav-links" style={{ display: window.innerWidth > 768 ? 'flex' : 'none', gap: '32px', alignItems: 'center', fontSize: '0.95rem', color: '#888', fontWeight: 500 }}>
+<div className="nav-links" style={{ gap: '32px', alignItems: 'center', fontSize: '0.95rem', color: '#888', fontWeight: 500 }}>

The existing CSS rule at line 539-541 already handles hiding .nav-links on mobile:

.nav-links {
    display: none;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/LandingPage/index.jsx` at line 122, The inline style in
the LandingPage JSX uses window.innerWidth to toggle display for the element
with className "nav-links", which won't update on viewport resize; remove the
inline style and rely on the existing CSS media queries (styles for .nav-links
and .mobile-menu-btn) or implement a responsive state with a resize listener in
the LandingPage component: either delete the style prop on the element that
contains className "nav-links" so CSS controls visibility, or add a useState
(e.g., isMobile) and window resize handler in the LandingPage component to
update that state and conditionally render/assign a CSS class instead of using
window.innerWidth directly.

136-140: ⚠️ Potential issue | 🟡 Minor

Same window.innerWidth issue at lines 136 and 140.

These also evaluate once and won't respond to window resize. Use CSS media queries for responsive visibility instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/LandingPage/index.jsx` around lines 136 - 140, The JSX is
using window.innerWidth inline to decide visibility for the Link elements and
the mobile menu button (the Link to="/login", Link to="/signup" and the button
with className "mobile-menu-btn" and onClick calling setIsMobileMenuOpen), which
only evaluates once; remove these inline window.innerWidth checks and instead
control visibility via CSS media queries or responsive classes (e.g., add
distinct classNames like "desktop-only" and "mobile-only" to the Login/Start
Free Links and the mobile-menu-btn) and update the stylesheet to show/hide
.desktop-only and .mobile-only at the desired breakpoints so the UI responds to
window resizes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/pages/LandingPage/index.jsx`:
- Line 468: The Link element with className "btn-hero-secondary" currently has
text "Back to Console" but navigates to "/login"; update this to be consistent
by either changing the to prop on that Link to "/dashboard" if it should return
authenticated users to the console (update any conditional routing if needed),
or change the visible text to "Sign In" if it should send unauthenticated users
to the login page; locate the Link in LandingPage (the element with className
"btn-hero-secondary" and inline padding style) and apply the appropriate change.

In `@frontend/src/pages/LandingPage/style.css`:
- Line 5: Remove the unnecessary quotes around the Inter font-name in the
font-family declaration in LandingPage's stylesheet: locate the font-family line
(font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif;) and change 'Inter' to Inter (no quotes) so it complies with the
stylelint font-family-name-quotes rule while keeping the remaining fallbacks
intact.

---

Outside diff comments:
In `@frontend/src/pages/LandingPage/index.jsx`:
- Around line 385-386: In the LandingPage component update the paragraph text
that currently reads "Link your supabase buckets or Supabase Storage..." to use
the intended storage providers string (e.g., "Link your S3 buckets or Supabase
Storage..."); locate the JSX paragraph in
frontend/src/pages/LandingPage/index.jsx (the <p> element with inline styles
around line 385) and replace the redundant "supabase buckets" phrasing with "S3
buckets" so the copy correctly reflects BYO Storage options.
- Line 122: The inline style in the LandingPage JSX uses window.innerWidth to
toggle display for the element with className "nav-links", which won't update on
viewport resize; remove the inline style and rely on the existing CSS media
queries (styles for .nav-links and .mobile-menu-btn) or implement a responsive
state with a resize listener in the LandingPage component: either delete the
style prop on the element that contains className "nav-links" so CSS controls
visibility, or add a useState (e.g., isMobile) and window resize handler in the
LandingPage component to update that state and conditionally render/assign a CSS
class instead of using window.innerWidth directly.
- Around line 136-140: The JSX is using window.innerWidth inline to decide
visibility for the Link elements and the mobile menu button (the Link
to="/login", Link to="/signup" and the button with className "mobile-menu-btn"
and onClick calling setIsMobileMenuOpen), which only evaluates once; remove
these inline window.innerWidth checks and instead control visibility via CSS
media queries or responsive classes (e.g., add distinct classNames like
"desktop-only" and "mobile-only" to the Login/Start Free Links and the
mobile-menu-btn) and update the stylesheet to show/hide .desktop-only and
.mobile-only at the desired breakpoints so the UI responds to window resizes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 632d5050-f4c1-4081-afdf-35cc8a15be8d

📥 Commits

Reviewing files that changed from the base of the PR and between 6b23ae0 and aeecf4d.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • frontend/lint_results.txt
  • frontend/package.json
  • frontend/src/pages/LandingPage/index.jsx
  • frontend/src/pages/LandingPage/style.css
✅ Files skipped from review due to trivial changes (1)
  • frontend/lint_results.txt
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/package.json

<p style={{ fontSize: '1.25rem', color: '#a1a1aa', marginBottom: '3rem' }}>Join hundreds of developers building the future without the backend headaches.</p>
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'center', flexWrap: 'wrap' }}>
<Link to="/signup" className="btn-hero-primary" style={{ padding: '1rem 3rem' }}>Create Your Project</Link>
<Link to="/login" className="btn-hero-secondary" style={{ padding: '1rem 3rem' }}>Back to Console</Link>

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 | 🟡 Minor

"Back to Console" links to /login — text/destination mismatch.

The button text "Back to Console" suggests returning to the dashboard, but it links to the login page. Consider either:

  • Change link to /dashboard (for authenticated users), or
  • Change text to "Sign In" (for unauthenticated users)
Proposed fix (if targeting authenticated users)
-                        <Link to="/login" className="btn-hero-secondary" style={{ padding: '1rem 3rem' }}>Back to Console</Link>
+                        <Link to="/dashboard" className="btn-hero-secondary" style={{ padding: '1rem 3rem' }}>Back to Console</Link>
📝 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
<Link to="/login" className="btn-hero-secondary" style={{ padding: '1rem 3rem' }}>Back to Console</Link>
<Link to="/dashboard" className="btn-hero-secondary" style={{ padding: '1rem 3rem' }}>Back to Console</Link>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/LandingPage/index.jsx` at line 468, The Link element with
className "btn-hero-secondary" currently has text "Back to Console" but
navigates to "/login"; update this to be consistent by either changing the to
prop on that Link to "/dashboard" if it should return authenticated users to the
console (update any conditional routing if needed), or change the visible text
to "Sign In" if it should send unauthenticated users to the login page; locate
the Link in LandingPage (the element with className "btn-hero-secondary" and
inline padding style) and apply the appropriate change.

font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
background-color: #030303;
color: #fcfcfc;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;

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 | 🟡 Minor

Remove quotes around Inter font family name.

Per the stylelint font-family-name-quotes rule, custom font names that are valid CSS identifiers should not be quoted.

Proposed fix
-    font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+    font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
📝 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
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
🧰 Tools
🪛 Stylelint (17.3.0)

[error] 5-5: Unexpected quotes around "Inter" (font-family-name-quotes)

(font-family-name-quotes)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/LandingPage/style.css` at line 5, Remove the unnecessary
quotes around the Inter font-name in the font-family declaration in
LandingPage's stylesheet: locate the font-family line (font-family: 'Inter',
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;) and change
'Inter' to Inter (no quotes) so it complies with the stylelint
font-family-name-quotes rule while keeping the remaining fallbacks intact.

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