Skip to content

chore: Phase 3 code review - API routes#7

Merged
AutomatosAI merged 2 commits intomainfrom
review/phase3-api
Jan 13, 2026
Merged

chore: Phase 3 code review - API routes#7
AutomatosAI merged 2 commits intomainfrom
review/phase3-api

Conversation

@AutomatosAI
Copy link
Copy Markdown
Owner

@AutomatosAI AutomatosAI commented Jan 13, 2026

Summary by CodeRabbit

  • New Features

    • Token-based password reset with expiry validation
    • Rate limiting for bulk tenant/product operations
    • Enhanced payment webhooks for crypto and fiat flows
    • Audit logs: filtering and paginated results
    • Improved template upload fallback and branding file uploads
    • Orders API now returns standardized success wrapper and includes product names
  • Bug Fixes

    • Product sync includes richer metadata
    • Tenant lookup fallback for shared conditions
    • Hardening of webhook error handling
  • Style

    • Widespread code formatting and consistency updates

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 13, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

A large codebase-wide normalization of string quotes plus targeted functional changes: password-reset token validation and reset, order submission flow (Dr. Green integration, webhook + email triggers, response shape), new/rewritten webhook handlers, rate-limit and authorization enhancements, pagination and tenant-scoped safeguards across many admin routes.

Changes

Cohort / File(s) Summary
Auth & Password Reset
nextjs_space/app/api/auth/[...nextauth]/route.ts, nextjs_space/app/api/auth/reset-password/confirm/route.ts, nextjs_space/app/api/auth/reset-password/route.ts, nextjs_space/app/api/reset-password/route.ts
Quote normalization across files; reset-password/confirm adds token lookup, expiry validation, password hashing and user update (clears token/expiry).
Orders & Checkout
nextjs_space/app/api/orders/route.ts, nextjs_space/app/api/orders/customer/route.ts, nextjs_space/app/api/store/[slug]/orders/submit/route.ts, nextjs_space/app/api/store/[slug]/orders/route.ts, nextjs_space/app/api/store/[slug]/orders/[orderId]/route.ts
Response shape change (POST returns { success: true, order }), productName added to order items, reflowed order submission with Dr. Green call, webhook ORDER_CREATED trigger, richer error handling and notes composition.
Store Cart Operations
nextjs_space/app/api/store/[slug]/cart/add/route.ts, nextjs_space/app/api/store/[slug]/cart/remove/route.ts, nextjs_space/app/api/store/[slug]/cart/clear/route.ts, nextjs_space/app/api/store/[slug]/cart/route.ts
Reworked to early-returns and linear validation, explicit size/quantity checks, tenant checks earlier, consistent error payloads; mostly quote/style normalization.
Dr. Green / Shop Registration / Products
nextjs_space/app/api/shop/register/route.ts, nextjs_space/app/api/doctor-green/products/route.ts, nextjs_space/app/api/doctor-green/sync-products.disabled/route.ts, nextjs_space/app/api/store/[slug]/products/route.ts
shop registration payload restructured (nested medicalRecord); product sync/route formatting; sync upsert includes richer product metadata (images, stock fields).
Webhooks (DrGreen) & Payments
nextjs_space/app/api/webhooks/drgreen/crypto/route.ts, nextjs_space/app/api/webhooks/drgreen/fiat/route.ts
New/rewritten handlers: parse vendor payloads, map external status → internal paymentStatus, update orders, audit/log, and trigger downstream webhooks (ORDER_CONFIRMED / ORDER_CANCELLED).
Super-Admin Email Templates & Mappings
nextjs_space/app/api/super-admin/email-templates/route.ts, nextjs_space/app/api/super-admin/email-templates/[id]/route.ts, nextjs_space/app/api/super-admin/email-mappings/route.ts
Enforced SUPER_ADMIN checks, add/standardize GET/PUT/DELETE flows, add validation/upsert behavior for system mappings; try/catch and consistent error shapes.
Tenant Admin: Email Templates & Mappings
nextjs_space/app/api/tenant-admin/email-templates/route.ts, nextjs_space/app/api/tenant-admin/email-templates/[id]/route.ts, nextjs_space/app/api/tenant-admin/email-templates/clone/route.ts, nextjs_space/app/api/tenant-admin/email-mappings/route.ts
Rewrites to explicit tenant checks, try/catch, validation; clone supports copying source template; mappings POST/DELETE moved to explicit upsert/delete with ownership checks.
Tenant Admin: Orders & Admin Actions
nextjs_space/app/api/tenant-admin/orders/route.ts, nextjs_space/app/api/tenant-admin/orders/[id]/route.ts, nextjs_space/app/api/tenant-admin/orders/[id]/admin-notes/route.ts, nextjs_space/app/api/tenant-admin/orders/bulk/route.ts
Added tenant-scoped user retrieval, tighter role checks, enhanced filters (search OR, date ranges), pagination, parallel aggregates; admin-notes enforces tenant-scoped authorization.
Rate Limiting & Analytics
nextjs_space/app/api/tenant-admin/analytics/route.ts, nextjs_space/app/api/tenant-admin/audit-logs/route.ts, nextjs_space/app/api/super-admin/analytics/route.ts, nextjs_space/app/api/super-admin/audit-logs/route.ts
Added checkRateLimit calls in several routes, dynamic filtering/pagination for audit logs, and small analytics query normalizations.
Tenant Admin: Products Bulk & Reorder
nextjs_space/app/api/tenant-admin/products/bulk/route.ts, nextjs_space/app/api/tenant-admin/products/reorder/route.ts
Added rate-limit, tenant validation, product ownership checks, improved bulk audit metadata; minor formatting changes.
Branding, Uploads & Templates
nextjs_space/app/api/tenant-admin/branding/route.ts, nextjs_space/app/api/tenant-admin/branding/upload/route.ts, nextjs_space/app/api/tenant-admin/templates/clone/route.ts, nextjs_space/app/api/tenant-admin/templates/[id]/activate/route.ts, nextjs_space/app/api/tenant-admin/templates/clone/route.ts
POST delegating to PUT in branding; upload sanitization and consistent responses; template clone rewritten as sequential try/catch with S3 copy + tenant template creation.
Tenant Admin: Settings, SMTP & Select Template
nextjs_space/app/api/tenant-admin/settings/route.ts, nextjs_space/app/api/tenant-admin/settings/test-smtp/route.ts, nextjs_space/app/api/tenant-admin/select-template/route.ts
Formatting, explicit tenant checks, sanitization for select-template, test SMTP outer catch block; PUT for SEO pages enhanced (see SEO cohort).
Tenant Admin: SEO
nextjs_space/app/api/tenant-admin/seo/pages/route.ts, nextjs_space/app/api/tenant-admin/seo/posts/[id]/route.ts, nextjs_space/app/api/tenant-admin/seo/products/[id]/route.ts
pages PUT enhanced: validate pageKey, trim fields, nullify removals and merge; other SEO endpoints normalized and formatted.
Tenant & Public Tenant Routes
nextjs_space/app/api/tenant/[slug]/route.ts, nextjs_space/app/api/tenant/current/route.ts, nextjs_space/app/api/tenant/conditions/route.ts, nextjs_space/app/api/tenant/conditions/[slug]/route.ts
tenant/[slug] adds early slug validation + 404; conditions/[slug] added fallback to master tenant (healingbuds) if not found; widespread quote normalization.
Miscellaneous: Health, Signup, Onboarding, Customer, User Profile, Consultation
nextjs_space/app/api/health/route.ts, nextjs_space/app/api/signup/route.ts, nextjs_space/app/api/onboarding/route.ts, nextjs_space/app/api/customer/*, nextjs_space/app/api/user/profile/route.ts, nextjs_space/app/api/consultation/*
Mostly quote/format normalization; onboarding adds crypto.randomUUID() for tenant id in payload; consultation submit had large refactor of payload assembly and Dr. Green integration—requires careful review.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant API as Order Submit Endpoint
    participant DB as Database
    participant DrGreen as Dr. Green API
    participant Webhook as Webhook Service
    participant Email as Email Service

    Client->>API: POST /store/[slug]/orders/submit (order + shipping)
    API->>API: Validate auth, body, tenant, shipping
    API->>DrGreen: submitOrder(payload, credentials)
    DrGreen-->>API: drGreenOrderId / error
    API->>DB: create order record (items, productName, totals)
    API->>DB: create audit log
    API->>Webhook: trigger ORDER_CREATED (orderId, drGreenOrderId, total, userEmail)
    API->>Email: send confirmation email
    API-->>Client: 201 { success: true, order }
Loading
sequenceDiagram
    actor External as External Payment Webhook
    participant API as Webhook Handler
    participant DB as Database
    participant Audit as Audit Log
    participant BudStack as BudStack Webhook

    External->>API: POST payment notification (fiat/crypto)
    API->>API: Parse payload, validate nonce/reference
    API->>DB: find order by reference/nonce
    alt order found
        API->>DB: update paymentStatus, invoiceNum, clear nonce
        API->>Audit: record webhook receipt
        alt payment PAID
            API->>DB: set order status CONFIRMED
            API->>BudStack: trigger ORDER_CONFIRMED webhook
        else payment FAILED
            API->>BudStack: trigger ORDER_CANCELLED webhook
        end
        API-->>External: 200 { status, orderId, paymentStatus }
    else not found
        API->>Audit: log failed lookup
        API-->>External: 404 { error }
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

codex

Poem

🐰
"I hopped through quotes from ' to " with delight,
fixed tokens, webhooks, and orders by moonlight.
Dr. Green now speaks, webhooks sing true,
emails fly out and carts march through.
A tidy patch — nibble, hop, and rewrite!"

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.58% 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 accurately describes the PR's primary purpose: a code review phase 3 focused on API routes formatting and improvements.

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

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 94ccbe8 and 22688f8.

📒 Files selected for processing (17)
  • nextjs_space/app/api/auth/reset-password/route.ts
  • nextjs_space/app/api/consultation/submit/route.ts
  • nextjs_space/app/api/onboarding/route.ts
  • nextjs_space/app/api/reset-password/route.ts
  • nextjs_space/app/api/shop/register/route.ts
  • nextjs_space/app/api/signup/route.ts
  • nextjs_space/app/api/super-admin/platform-settings/route.ts
  • nextjs_space/app/api/super-admin/templates/[id]/route.ts
  • nextjs_space/app/api/super-admin/tenants/bulk/route.ts
  • nextjs_space/app/api/super-admin/tenants/route.ts
  • nextjs_space/app/api/tenant-admin/analytics/route.ts
  • nextjs_space/app/api/tenant-admin/orders/bulk/route.ts
  • nextjs_space/app/api/tenant-admin/products/bulk/route.ts
  • nextjs_space/app/api/tenant-admin/products/reorder/route.ts
  • nextjs_space/app/api/tenant-admin/select-template/route.ts
  • nextjs_space/app/api/tenant-admin/settings/route.ts
  • nextjs_space/app/api/user/profile/route.ts

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (9)
nextjs_space/app/api/consultation/submit/route.ts (3)

38-53: Fallback returns invalid format for unmapped countries.

When a country code isn't in the mapping, the function returns the original Alpha-2 code. Since the Dr. Green API requires Alpha-3 codes (per the docstring), this will cause API failures for customers from unmapped countries.

Consider either:

  1. Throwing an error for unsupported countries with a clear message
  2. Using a comprehensive mapping library like i18n-iso-countries
💡 Option 1: Fail explicitly for unmapped countries
 function convertToAlpha3CountryCode(alpha2: string): string {
   const mapping: Record<string, string> = {
     PT: "PRT", // Portugal
     // ... existing mappings
   };
-  return mapping[alpha2.toUpperCase()] || alpha2;
+  const alpha3 = mapping[alpha2.toUpperCase()];
+  if (!alpha3) {
+    throw new Error(`Unsupported country code: ${alpha2}. Please add mapping for this country.`);
+  }
+  return alpha3;
 }

70-81: Missing input validation before processing.

The handler directly accesses body.password, body.email, etc. without validation. If any required field is missing or malformed:

  • bcrypt.hash(body.password, 10) will throw if body.password is undefined
  • body.email could be undefined causing the findUnique to fail

Consider adding validation at the start of the handler or using a schema validation library (e.g., Zod).

💡 Example validation
export async function POST(request: NextRequest) {
  try {
    const body = await request.json();

    // Basic validation
    if (!body.email || !body.password || !body.firstName || !body.lastName) {
      return NextResponse.json(
        { success: false, error: "Missing required fields" },
        { status: 400 }
      );
    }

    // ... rest of handler

115-117: Defaulting dateOfBirth to current date is incorrect.

If body.dateOfBirth is falsy, the code defaults to new Date() (today). For a medical consultation system, date of birth should be a required field, not silently defaulted to an incorrect value. This could lead to incorrect age calculations in medical records.

💡 Suggested fix
-        dateOfBirth: body.dateOfBirth ? new Date(body.dateOfBirth) : new Date(),
+        dateOfBirth: new Date(body.dateOfBirth), // Ensure validation requires this field

Add to validation:

if (!body.dateOfBirth) {
  return NextResponse.json(
    { success: false, error: "Date of birth is required" },
    { status: 400 }
  );
}
nextjs_space/app/api/doctor-green/sync-products.disabled/route.ts (2)

10-23: Security: Missing authentication allows arbitrary tenant impersonation.

The tenantId is taken directly from the request body without any authentication or authorization check. Any caller can sync products for any tenant by simply providing a different tenantId. The comment on line 11 acknowledges this needs to be fixed, but this is a critical security gap if the route is ever enabled.

Additionally, the dynamic import on line 22 is unnecessary—getTenantDrGreenConfig can be imported statically at the top of the file.

🔐 Recommended fix: Add authentication and use static import
 import { NextRequest, NextResponse } from "next/server";
 import { fetchProducts } from "@/lib/doctor-green-api";
 import { prisma } from "@/lib/db";
+import { getTenantDrGreenConfig } from "@/lib/tenant-config";
+import { getServerSession } from "next-auth"; // or your auth method
+import { authOptions } from "@/lib/auth";

 export async function POST(request: NextRequest) {
   try {
-    // Get tenant ID from request (will be from auth session in production)
-    const { tenantId } = await request.json();
+    // Authenticate and get tenant ID from session
+    const session = await getServerSession(authOptions);
+    if (!session?.user?.tenantId) {
+      return NextResponse.json(
+        { success: false, error: "Unauthorized" },
+        { status: 401 },
+      );
+    }
+    const tenantId = session.user.tenantId;

-    if (!tenantId) {
-      return NextResponse.json(
-        { success: false, error: "Tenant ID required" },
-        { status: 400 },
-      );
-    }
-
-    // Fetch tenant-specific Dr Green Config
-    const { getTenantDrGreenConfig } = await import("@/lib/tenant-config");
     const doctorGreenConfig = await getTenantDrGreenConfig(tenantId);

54-76: Critical: Code uses non-existent database fields and has TOCTOU race condition.

The code attempts to use fields (doctorGreenId, doctorGreenData, image, inStock, stockQuantity, lastSyncedAt) that don't exist in the products schema. This will cause runtime Prisma validation errors. Additionally, the findFirst + conditional update/create pattern introduces a TOCTOU race condition and N+1 query inefficiency (2 queries per product).

Before optimization, the schema must be updated to include the missing doctor-green integration fields. Once added, replace the manual check-then-write pattern with Prisma's atomic upsert method wrapped in a transaction.

nextjs_space/app/api/tenant-admin/products/reorder/route.ts (1)

29-39: SUPER_ADMIN users will receive a 404 error due to missing tenantId.

SUPER_ADMIN users created in the seed lack a tenantId (the field is optional in the user model), but the code requires it and returns a 404 if missing. This is a systemic issue across tenant-admin routes (products/bulk, orders, etc.). Either assign tenantId to SUPER_ADMIN users during creation, or add conditional logic to handle SUPER_ADMIN users differently—for example, allowing them to reorder products across any tenant or requiring tenantId as a request parameter.

nextjs_space/app/api/tenant-admin/orders/route.ts (1)

396-408: Fix relation names in PATCH handler to match Prisma schema.

The PATCH handler uses incorrect relation names: items and user instead of the schema-defined order_items and users. This will cause a runtime error when updating orders. The GET handler already uses the correct names.

Update lines 401-402 to:

        order_items: true,
        users: {
nextjs_space/app/api/super-admin/tenants/[id]/route.ts (1)

223-246: Missing audit log for tenant deletion.

The DELETE handler doesn't create an audit log entry, unlike PATCH which logs TENANT_UPDATED. Deleting a tenant is a significant action that should be tracked for compliance and debugging purposes.

Suggested fix
 export async function DELETE(
   req: NextRequest,
   { params }: { params: { id: string } },
 ) {
   try {
     const session = await getServerSession(authOptions);

     if (!session || session.user.role !== "SUPER_ADMIN") {
       return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
     }

+    // Get tenant info before deletion for audit log
+    const tenant = await prisma.tenants.findUnique({
+      where: { id: params.id },
+      select: { businessName: true, subdomain: true },
+    });
+
+    if (!tenant) {
+      return NextResponse.json({ error: "Tenant not found" }, { status: 404 });
+    }
+
     // Delete tenant (cascade will handle related records)
     await prisma.tenants.delete({
       where: { id: params.id },
     });

+    // Create audit log
+    await prisma.audit_logs.create({
+      data: {
+        id: crypto.randomUUID(),
+        action: "TENANT_DELETED",
+        entityType: "Tenant",
+        entityId: params.id,
+        userId: session.user.id,
+        userEmail: session.user.email,
+        metadata: {
+          businessName: tenant.businessName,
+          subdomain: tenant.subdomain,
+        },
+      },
+    });
+
     return NextResponse.json({ success: true });
   } catch (error) {
nextjs_space/app/api/super-admin/templates/upload/route.ts (1)

106-121: Error message shows wrong HTTP status code when master branch download fails.

When both main and master branch downloads fail, the error message incorrectly references response.status (from the main request) instead of masterResponse.status (from the master request that actually failed).

Suggested fix
        const masterResponse = await fetch(masterUrl);
        if (!masterResponse.ok) {
          throw new Error(
-           `Failed to download repository. Status: ${response.status}`,
+           `Failed to download repository. Tried main (${response.status}) and master (${masterResponse.status})`,
          );
        }
🤖 Fix all issues with AI agents
In @nextjs_space/app/api/consultation/submit/route.ts:
- Line 197: The shipping address object currently sets address2: body.address2
which is the wrong field name and causes address line 2 to be blank; update the
assignment to use body.addressLine2 (matching the questionnaire creation and
form) wherever the shippingAddress is built in the submit route (e.g., in the
route handler that constructs the shippingAddress object) so the API sends the
correct addressLine2 value to Dr. Green.

In @nextjs_space/app/api/reset-password/route.ts:
- Line 44: The code currently hardcodes name: "Gerard Kavanagh" for new users;
replace this by deriving the name from the incoming request (e.g., read a name
field from the request body) or compute a fallback from the email local-part,
and update the object that contains name: "Gerard Kavanagh" so it uses the
request-provided name (or a sanitized email-derived string) instead; locate the
literal "Gerard Kavanagh" in route.ts and modify the user-creation payload there
to use the request body param (e.g., req.body.name / parsed JSON) or a helper
that extracts the part before '@' from the email.
- Around line 8-88: This endpoint's POST handler allows unauthenticated password
resets and account creation (see function POST) and contains hardcoded values
("Gerard Kavanagh", role "PATIENT", tenant subdomain "healingbuds"), enabling
account takeover and user enumeration; remove the entire route file immediately
(delete the POST handler / file) or disable its export so it is not mounted, and
ensure no references remain; if temporary access is required, replace with
proper guards by delegating to the existing secure flows (the token-based reset
endpoints) rather than performing direct create/update operations.

In @nextjs_space/app/api/super-admin/platform-settings/route.ts:
- Around line 109-131: The GET handler lacks the super-admin auth guard; mirror
the POST auth check by calling getServerSession(req) at the top of GET, validate
the session and that session.user.role === SUPER_ADMIN (use the same SUPER_ADMIN
constant and options used in the POST), and return an appropriate NextResponse
(401/403 JSON) when the check fails before querying prisma.platform_settings;
refer to the existing POST function in this file for the exact session-check
logic and error responses and reuse the same behavior.

In @nextjs_space/app/api/super-admin/tenants/route.ts:
- Around line 172-207: Inside the prisma.$transaction callback, replace the
incorrect model references tx.tenant, tx.user, and tx.tenantBranding with the
schema-correct model names tx.tenants, tx.users, and tx.tenant_branding
respectively (so newTenant creation, admin user creation, and default branding
use those models), and change the tx parameter type from any to
Prisma.TransactionClient to restore type safety and proper validation.

In @nextjs_space/app/api/tenant-admin/posts/[id]/route.ts:
- Around line 109-121: The loop that regenerates uniqueSlug calls
prisma.posts.findFirst with tenantId set to user!.tenant!.id which will throw
because the user relation is tenants (plural) and the user row already has
tenantId; change the tenant lookup to use user!.tenantId instead (update the
prisma.posts.findFirst call inside the slug uniqueness loop and the assignment
to dataToUpdate.slug), ensuring you reference the same user variable used
earlier and keep the rest of the uniqueness logic intact.

In @nextjs_space/app/api/tenant-admin/products/bulk/route.ts:
- Around line 104-116: The current bulk handler overwrites real inventory counts
by setting the Int `stock` to 1/0 via the `newStock` variable and
`prisma.products.updateMany`, which destroys actual quantities; change the
update to toggle a boolean availability flag instead (e.g., set `isAvailable`
based on `action === "set-in-stock"`) and leave `stock` untouched, or if you
must preserve/restore counts first call `prisma.products.findMany` for the
target IDs to capture current `stock` values and persist them to an audit/backup
table/field before running the update; update references to `newStock`, the
`prisma.products.updateMany` call, and the `auditAction` assignment accordingly
so you no longer overwrite `stock`.

In @nextjs_space/app/api/tenant-admin/webhooks/[id]/deliveries/route.ts:
- Around line 11-14: The GET handler currently assumes params is synchronous;
update the signature to accept params as a Promise and await it before use:
change the parameter type to { params: Promise<{ id: string }> } (i.e., export
async function GET(req: NextRequest, { params }: { params: Promise<{ id: string
}> }) ) and then do const { id } = await params (or await params wherever params
is accessed) so the dynamic route works on Next.js 15 where params is a Promise.

In @nextjs_space/app/api/tenant-admin/webhooks/[id]/route.ts:
- Around line 12-14: The PATCH and DELETE handlers (export async function PATCH
and export async function DELETE) access params synchronously but must support
Next.js 15 where params is a Promise; change the handlers to await params (e.g.,
const { id } = await params) before using it and update the params typing to
Promise<{ id: string }> so both PATCH and DELETE correctly await and extract id.

In @nextjs_space/app/api/webhooks/drgreen/crypto/route.ts:
- Around line 154-167: The code attempts to call request.json() again inside the
error-logging block (used by prisma.drGreenWebhookLog.create) but the request
body stream was already consumed earlier, so capture and reuse the parsed body
instead: parse the body once into a variable (e.g., parsedBody) in the outer
scope where the request is first read, then reference that variable in the
catch/error-logging path for prisma.drGreenWebhookLog.create (and provide a safe
fallback value like "unavailable" or null if parsing failed) instead of calling
request.json() a second time.

In @nextjs_space/app/api/webhooks/drgreen/fiat/route.ts:
- Around line 96-127: The webhook handler uses order.user.email and
parseFloat(amount || "0") unsafely; update the triggerWebhook calls (the blocks
invoking triggerWebhook for WEBHOOK_EVENTS.ORDER_CONFIRMED and ORDER_CANCELLED)
to read the customer email via a null-safe expression (e.g., get a local
customerEmail = order.user?.email ?? "" or a clearly named fallback) and pass
that instead of order.user.email, and validate/normalize amount before sending
(e.g., parseFloat(amount) with an isNaN check or Number() and fallback to 0) so
the data object always contains a safe string for customerEmail and a finite
numeric amount for the payment payload.
🟠 Major comments (13)
nextjs_space/app/api/shop/register/route.ts-37-42 (1)

37-42: Fail early when credentials are missing.

The code logs an error when API credentials are missing but continues execution, deferring failure to createClient. This makes debugging harder and returns a generic error message. Resolve the TODO-like comment and fail fast with a clear error.

🐛 Proposed fix
     if (!config.apiKey || !config.secretKey) {
       console.error("Missing Dr. Green API credentials for registration");
-      // Continue anyway? The createClient might fail or we should block.
-      // createClient throws 'MISSING_CREDENTIALS` if keys are missing.
-      // We'll let it proceed and fail inside if needed, or handle it here.
+      return NextResponse.json(
+        { error: "Service configuration error. Please contact support." },
+        { status: 500 },
+      );
     }
nextjs_space/app/api/doctor-green/products/route.ts-12-12 (1)

12-12: Dead code: tenant country fallback logic is unreachable.

Line 12 ensures country is always truthy (defaults to "PT"), which means the fallback logic on line 36 (tenant.countryCode || "SA") will never execute. The tenant-specific country configuration is effectively ignored.

If the intent is to respect tenant country settings when no query parameter is provided, remove the default from line 12:

-    const country = searchParams.get("country") || "PT";
+    const country = searchParams.get("country");

Then line 36 will correctly fall back through: query param → tenant config → "SA" default.

Also applies to: 36-36

nextjs_space/app/api/tenant-admin/email-mappings/route.ts-185-195 (1)

185-195: Unexpected template deletion - potential data loss.

Deleting a mapping also deletes the associated template (lines 190-194). This is unexpected behavior for an endpoint managing "mappings" and could cause data loss if:

  1. The template is used by other mappings
  2. The user intends to reassign the template later
  3. The template contains significant customization work

Additionally, these operations lack transaction safety - if the template deletion fails after the mapping is deleted, data is left inconsistent.

🔧 Suggested fix - remove template deletion or make it explicit

Option 1: Remove automatic template deletion (recommended):

     if (mapping) {
       // Delete Mapping
       await prisma.email_event_mappings.delete({ where: { id: mapping.id } });
-
-      // Delete Template ONLY IF it belongs to tenant (Safety check)
-      if (mapping.template && mapping.template.tenantId === tenantId) {
-        await prisma.email_templates.delete({
-          where: { id: mapping.template.id },
-        });
-      }
     }

Option 2: If template deletion is intentional, use a transaction and make it opt-in via query param:

+    const deleteTemplate = searchParams.get("deleteTemplate") === "true";
+
-    if (mapping) {
-      await prisma.email_event_mappings.delete({ where: { id: mapping.id } });
-      if (mapping.template && mapping.template.tenantId === tenantId) {
-        await prisma.email_templates.delete({
-          where: { id: mapping.template.id },
-        });
-      }
-    }
+    if (mapping) {
+      await prisma.$transaction(async (tx) => {
+        await tx.email_event_mappings.delete({ where: { id: mapping.id } });
+        if (deleteTemplate && mapping.template?.tenantId === tenantId) {
+          await tx.email_templates.delete({ where: { id: mapping.template.id } });
+        }
+      });
+    }
nextjs_space/app/api/tenant-admin/upload/route.ts-17-22 (1)

17-22: Add file type and size validation for upload security.

The route lacks validation for file type and size, which poses security risks:

  • Unrestricted file types could allow upload of malicious content (HTML, SVG with scripts)
  • No size limit enables potential DoS through large uploads

Additionally, the filename is not sanitized before upload, unlike the similar branding/upload/route.ts route which sanitizes with file.name.replace(/[^a-zA-Z0-9.-]/g, "_").

🔒 Proposed fix with validation
     const formData = await req.formData();
     const file = formData.get("file") as File;

     if (!file) {
       return NextResponse.json({ error: "No file provided" }, { status: 400 });
     }
+
+    // Validate file type
+    const allowedTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
+    if (!allowedTypes.includes(file.type)) {
+      return NextResponse.json(
+        { error: "Invalid file type. Only images are allowed." },
+        { status: 400 }
+      );
+    }
+
+    // Validate file size (e.g., 5MB limit)
+    const maxSize = 5 * 1024 * 1024;
+    if (file.size > maxSize) {
+      return NextResponse.json(
+        { error: "File too large. Maximum size is 5MB." },
+        { status: 400 }
+      );
+    }
nextjs_space/app/api/tenant-admin/upload/route.ts-62-70 (1)

62-70: Return only the key from upload endpoint, or use signed URLs if returning URLs.

The bucket is private (confirmed by getFileUrl generating signed URLs with 1-hour expiration), but the code returns an unsigned public S3 URL that will fail on access. This causes:

  • Broken images when the returned URL is used directly
  • Inconsistency with branding/upload/route.ts, which returns only the key

Choose one approach:

  1. Return only the key and let consumers call getFileUrl() for signed URLs (matches branding/upload/route.ts)
  2. Return a signed URL by calling getFileUrl(key) before responding

Currently the "Optimistic public URL" comment is misleading for a private bucket.

nextjs_space/app/api/customer/change-password/route.ts-46-57 (1)

46-57: Handle users without a password (OAuth-only accounts).

If a user registered via OAuth/social login, user.password may be null. Calling bcrypt.compare(oldPassword, null) will throw an error or behave unexpectedly. Add a null check before comparison.

🔧 Proposed fix
     if (!user) {
       return NextResponse.json({ error: "User not found" }, { status: 404 });
     }

+    // Check if user has a password (OAuth-only users may not)
+    if (!user.password) {
+      return NextResponse.json(
+        { error: "Password change not available. You may have signed up using a social login." },
+        { status: 400 },
+      );
+    }
+
     // Verify old password
     const isValidPassword = await bcrypt.compare(oldPassword, user.password);
nextjs_space/app/api/super-admin/tenants/[id]/reset-password/route.ts-60-84 (1)

60-84: Misleading success response: email is not actually sent.

The response at line 82 claims "Password reset email sent successfully", but the actual email sending is commented out (lines 60-62). This will mislead both the caller and the admin user who won't receive the reset link.

Either implement the email sending or update the response message to accurately reflect what happened.

Suggested fix
-    // TODO: Send email with reset link
-    // const resetLink = `${process.env.NEXTAUTH_URL}/auth/reset-password/${resetToken}`;
-    // await sendPasswordResetEmail(adminUser.email, resetLink);
+    // Send email with reset link
+    const resetLink = `${process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"}/auth/reset-password/${resetToken}`;
+    const html = await emailTemplates.passwordReset(
+      adminUser.name || "User",
+      resetLink,
+      "BudStack",
+    );
+    await sendEmail({
+      to: adminUser.email,
+      subject: "Password Reset Request",
+      html,
+      tenantId: params.id,
+      templateName: "passwordReset",
+    });

     // Create audit log
     ...

     return NextResponse.json({
-      message: "Password reset email sent successfully",
+      message: "Password reset initiated successfully",
       email: adminUser.email,
     });

Based on the relevant code snippet from nextjs_space/app/api/auth/reset-password/route.ts (lines 42-53), the codebase already has an implementation pattern for sending password reset emails using emailTemplates.passwordReset and sendEmail.

nextjs_space/app/api/webhooks/drgreen/fiat/route.ts-10-29 (1)

10-29: Add PayIn webhook signature verification.

This endpoint accepts payment notifications without verifying authenticity. PayIn webhooks include an X-PayIn-Signature header (format: t=<timestamp>,v1=<hex_signature>) that must be validated against the webhook secret using HMAC-SHA256 to prevent spoofed payloads.

Implement signature verification by:

  1. Reading the raw request body as Buffer (before JSON parsing)
  2. Extracting the timestamp and signature from the header
  3. Computing HMAC-SHA256 over "<timestamp>.<raw_body>" using your PayIn webhook secret
  4. Comparing with the v1 value using constant-time comparison

Also consider rejecting requests with timestamps older than 5 minutes to mitigate replay attacks.

nextjs_space/app/api/signup/route.ts-30-35 (1)

30-35: Terms acceptance should be persisted for compliance.

The acceptTerms field is validated but not stored in the database. For GDPR/CCPA compliance, you should typically record when and that the user accepted the terms. Consider adding fields like termsAcceptedAt to the user record.

🔧 Suggested fix
     // Create user
     const user = await prisma.users.create({
       data: {
         email,
         password: hashedPassword,
         name: `${firstName} ${lastName}`,
         role: "PATIENT",
         tenantId: tenant.id,
+        termsAcceptedAt: new Date(),
       },
     });

Note: This requires adding a termsAcceptedAt DateTime? field to your Prisma schema.

nextjs_space/app/api/tenant-admin/tenant/route.ts-14-18 (1)

14-18: Missing role authorization check.

This endpoint only verifies authentication (session?.user?.id) but doesn't verify the user has TENANT_ADMIN or SUPER_ADMIN role. All other routes under /api/tenant-admin/ consistently check for these roles—including customers/route.ts, settings/route.ts, branding/route.ts, orders/route.ts, and email-templates/route.ts.

This endpoint's docstring states it's "for the authenticated tenant admin," yet any authenticated user (including patients) could access it. Add an explicit role check to match the authorization pattern used throughout the rest of the tenant-admin endpoints.

Suggested fix: Add role check
     if (!session?.user?.id) {
       return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
     }
+
+    if (!["TENANT_ADMIN", "SUPER_ADMIN"].includes(session.user.role || "")) {
+      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+    }
nextjs_space/app/api/super-admin/email-templates/[id]/route.ts-100-107 (1)

100-107: Incomplete handling for system template deletion.

The code fetches the template to check isSystem but then does nothing with this information (lines 100-103 are a no-op). System templates may be referenced by email_event_mappings, and deleting them could break email functionality.

Either prevent deletion of system templates or ensure referential integrity by checking for mappings first.

🛡️ Suggested: Prevent system template deletion
     if (template?.isSystem) {
-      // Option: prevent delete, or allow but with warning.
-      // For now, let's allow it but maybe frontend warns.
+      return NextResponse.json(
+        { error: "Cannot delete system templates" },
+        { status: 403 },
+      );
     }
nextjs_space/app/api/tenant-admin/email-templates/route.ts-88-102 (1)

88-102: Missing access validation when copying from source template.

When a sourceTemplateId is provided, the code fetches the template without validating that the requesting tenant has access to it. This could allow a tenant to copy content from another tenant's private template by knowing/guessing the ID.

The source template should be validated to ensure it's either owned by the current tenant or is a system template.

🔒 Proposed fix: Validate source template access
     if (sourceTemplateId) {
       const source = await prisma.email_templates.findUnique({
         where: { id: sourceTemplateId },
       });
-      if (source) {
+      // Validate access: must be system template or owned by this tenant
+      if (source && (source.isSystem || source.tenantId === user.tenants.id)) {
         data = {
           ...data,
           name: name || `${source.name} (Copy)`,
           subject: source.subject,
           contentHtml: source.contentHtml,
           category: source.category,
           description: source.description || `Copy of ${source.name}`,
         };
+      } else if (source) {
+        return NextResponse.json(
+          { error: "Access denied to source template" },
+          { status: 403 },
+        );
       }
     }
nextjs_space/app/api/webhooks/drgreen/crypto/route.ts-14-18 (1)

14-18: Implement authentication for CoinRemitter webhook endpoint.

This endpoint accepts and processes payments without any authentication. CoinRemitter's public documentation does not provide built-in webhook signature verification, but the endpoint should still implement protection. An attacker who discovers the URL could send forged payloads to mark orders as paid (lines 103-111 mark orders as "CONFIRMED" on PAID status).

Implement one or more of the following:

  • Require a secret token (configured in CoinRemitter settings or as a query parameter) and validate it server-side
  • Verify payment status by calling CoinRemitter's API using your API key before updating the order
  • Use IP whitelisting if CoinRemitter publishes their webhook server IPs
  • Add HTTPS + authentication headers as defense-in-depth
🟡 Minor comments (17)
nextjs_space/app/api/tenant-admin/seo/pages/route.ts-58-59 (1)

58-59: Add error handling for JSON parsing.

Same issue as the products route - request.json() can throw on malformed JSON.

Proposed fix
-  const body = await request.json();
-  const { pageKey, seo } = body;
+  let body;
+  try {
+    body = await request.json();
+  } catch {
+    return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
+  }
+  const { pageKey, seo } = body;
nextjs_space/app/api/tenant-admin/seo/products/[id]/route.ts-77-78 (1)

77-78: Add error handling for JSON parsing.

request.json() can throw a SyntaxError if the request body contains malformed JSON. Without a try/catch, this results in an unhandled exception returning a 500 error instead of a user-friendly 400 response.

Proposed fix
-  const body = await request.json();
-  const { title, description, ogImage } = body;
+  let body;
+  try {
+    body = await request.json();
+  } catch {
+    return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
+  }
+  const { title, description, ogImage } = body;
nextjs_space/app/api/consultation/submit/route.ts-307-315 (1)

307-315: External API call lacks timeout configuration.

The fetch call to Dr. Green API has no timeout. If the external service is slow or unresponsive, this request will hang indefinitely, potentially causing resource exhaustion.

💡 Add timeout with AbortController
+      const controller = new AbortController();
+      const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
+
       const response = await fetch(`${drGreenApiUrl}/dapp/clients`, {
         method: "POST",
         headers: {
           "Content-Type": "application/json",
           "x-auth-apikey": apiKey,
           "x-auth-signature": signature,
         },
         body: payloadStr,
+        signal: controller.signal,
       });
+
+      clearTimeout(timeoutId);
nextjs_space/app/api/tenant-admin/email-mappings/route.ts-104-111 (1)

104-111: Validate eventType against allowed values.

The eventType from user input is not validated against SYSTEM_EVENTS. This allows creation of mappings for arbitrary event types, which could lead to data inconsistency.

🔒 Suggested fix
     const { eventType, templateId } = await req.json();

-    if (!eventType || !templateId) {
+    if (!eventType || !templateId) {
+      return NextResponse.json(
+        { error: "Missing required fields" },
+        { status: 400 },
+      );
+    }
+
+    if (!SYSTEM_EVENTS.includes(eventType)) {
       return NextResponse.json(
-        { error: "Missing required fields" },
+        { error: "Invalid event type" },
         { status: 400 },
       );
     }
nextjs_space/app/api/customer/change-password/route.ts-68-81 (1)

68-81: Add tenantId to audit log for consistency.

Other audit log entries in the codebase (e.g., in tenant-admin/customers/[id]/reset-password/route.ts) include tenantId. This audit log is missing it, which could affect filtering and querying audit trails.

🔧 Proposed fix

First, update the user query to include tenantId (around line 43):

       where: { id: session.user.id },
-      select: { id: true, password: true, email: true },
+      select: { id: true, password: true, email: true, tenantId: true },
     });

Then add it to the audit log:

     await prisma.audit_logs.create({
       data: {
         id: crypto.randomUUID(),
         action: "PASSWORD_CHANGED",
         entityType: "User",
         entityId: session.user.id,
         userId: session.user.id,
         userEmail: session.user.email,
+        tenantId: user.tenantId || undefined,
         metadata: {
           method: "self-service",
         },
       },
     });
nextjs_space/app/api/doctor-green/sync-products.disabled/route.ts-38-41 (1)

38-41: Slug generation may cause collisions for products with identical names.

The slug is derived solely from dgProduct.name. If two products have the same name (or names that normalize to the same slug), this will cause conflicts on upsert or create operations if slug has a unique constraint. Consider appending the doctorGreenId or a hash to ensure uniqueness.

💡 Suggested fix
         slug: dgProduct.name
           .toLowerCase()
           .replace(/\s+/g, "-")
-          .replace(/[^\w-]/g, ""),
+          .replace(/[^\w-]/g, "") + `-${dgProduct.id}`,
nextjs_space/app/api/onboarding/route.ts-169-170 (1)

169-170: Remove duplicate comment.

Line 169 and 170 contain the same comment.

Proposed fix
     // Send tenant welcome email (don't wait for it)
-    // Send tenant welcome email (don't wait for it)
     const html = await emailTemplates.tenantWelcome(
nextjs_space/app/api/super-admin/tenants/route.ts-29-30 (1)

29-30: Add validation for pagination parameters.

parseInt can return NaN for invalid input, which would cause the Prisma query to fail or behave unexpectedly. Consider adding validation.

Suggested fix
-    const page = parseInt(searchParams.get("page") || "1");
-    const limit = parseInt(searchParams.get("limit") || "10");
+    const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10) || 1);
+    const limit = Math.min(100, Math.max(1, parseInt(searchParams.get("limit") || "10", 10) || 10));
nextjs_space/app/api/signup/route.ts-73-74 (1)

73-74: Remove duplicate comment.

Line 73 and 74 contain the same comment // Send welcome email (don't wait for it).

🧹 Fix
     // Send welcome email (don't wait for it)
-    // Send welcome email (don't wait for it)
     const html = await emailTemplates.welcome(
nextjs_space/app/api/user/profile/route.ts-27-38 (1)

27-38: Address object excludes addressLine2-only updates.

The condition addressLine1 || city || state || postalCode || country doesn't include addressLine2. If a user only provides addressLine2, the address object won't be built, and the field won't be updated.

If this is intentional (requiring at least one "primary" address field), consider documenting it. Otherwise:

Suggested fix
     const address =
-      addressLine1 || city || state || postalCode || country
+      addressLine1 || addressLine2 || city || state || postalCode || country
         ? {
             addressLine1: addressLine1 || "",
             addressLine2: addressLine2 || "",
nextjs_space/app/api/tenant-admin/settings/route.ts-59-66 (1)

59-66: Inconsistent error handling for encryption failures.

SMTP password encryption failure is silently caught (lines 63-65) while drGreenSecretKey encryption failure throws (line 81). This inconsistency could lead to:

  • SMTP settings saved without password, causing later authentication failures
  • Different user experience for similar failures

Consider either throwing for both or handling both gracefully with user feedback.

Suggested fix for consistent handling
     if (smtpPassword && smtpPassword.trim() !== "") {
       try {
         smtpSettings.password = encrypt(smtpPassword);
       } catch (e) {
         console.error("SMTP Password Encryption failed:", e);
+        return NextResponse.json(
+          { error: "Failed to encrypt SMTP password" },
+          { status: 500 },
+        );
       }
     }
nextjs_space/app/api/tenant-admin/customers/[id]/reset-password/route.ts-81-87 (1)

81-87: Email send failure is silent - user receives success response.

If sendEmail fails, the operation appears successful to the admin, but the customer never receives the reset email. The token is already saved to the database at this point.

Consider either:

  1. Awaiting email and returning an appropriate error if it fails
  2. Implementing a retry mechanism
  3. At minimum, logging when email fails and including a warning in the response
Suggested improvement
-    await sendEmail({
-      to: customer.email,
-      subject: "Password Reset Request",
-      html,
-      tenantId: customer.tenantId || "SYSTEM",
-      templateName: "passwordReset",
-    });
+    try {
+      await sendEmail({
+        to: customer.email,
+        subject: "Password Reset Request",
+        html,
+        tenantId: customer.tenantId || "SYSTEM",
+        templateName: "passwordReset",
+      });
+    } catch (emailError) {
+      console.error("Failed to send password reset email:", emailError);
+      // Consider: return error or include warning in response
+    }
nextjs_space/app/api/super-admin/email-mappings/route.ts-56-78 (1)

56-78: Potential race condition in upsert logic and missing templateId validation.

  1. Race condition: Two concurrent requests with the same eventType could both pass findFirst (returning null) and then both attempt create, causing a constraint violation if the schema enforces uniqueness. Consider wrapping in a transaction with a retry or using upsert with a compound unique constraint if supported.

  2. Missing templateId validation: The templateId is not verified to exist before creating/updating the mapping. If the template doesn't exist, this will fail with a foreign key error at the database level, which may result in a less informative 500 error.

💡 Suggested improvement: Validate templateId exists
+    // Verify template exists
+    const templateExists = await prisma.email_templates.findUnique({
+      where: { id: templateId },
+      select: { id: true },
+    });
+
+    if (!templateExists) {
+      return NextResponse.json(
+        { error: "Template not found" },
+        { status: 404 },
+      );
+    }
+
     // Safer approach for nullable unique:
     const existing = await prisma.email_event_mappings.findFirst({
nextjs_space/app/api/super-admin/email-templates/[id]/route.ts-60-72 (1)

60-72: Missing template existence check before update.

If the template with the given ID doesn't exist, prisma.email_templates.update will throw a RecordNotFoundError, resulting in a generic 500 response instead of a clear 404. Consider checking existence first or handling the specific Prisma error.

💡 Suggested improvement
   try {
     const body = await req.json();
     const { name, subject, contentHtml, description, category, isSystem, isActive } = body;
 
+    const existing = await prisma.email_templates.findUnique({
+      where: { id: params.id },
+      select: { id: true },
+    });
+
+    if (!existing) {
+      return NextResponse.json({ error: "Template not found" }, { status: 404 });
+    }
+
     const updated = await prisma.email_templates.update({
nextjs_space/app/api/webhooks/drgreen/crypto/route.ts-136-136 (1)

136-136: Potential NaN in webhook payload.

parseFloat(usd_amount || "0") will return NaN if usd_amount is a non-numeric string. Consider adding validation or using a fallback.

Suggested fix
-          amount: parseFloat(usd_amount || "0"),
+          amount: parseFloat(usd_amount) || 0,
nextjs_space/app/api/super-admin/templates/upload/route.ts-313-321 (1)

313-321: Error details may leak sensitive information.

Including error.message directly in the API response could expose internal paths, database errors, or other sensitive implementation details to clients. Consider logging the full error server-side but returning a generic message to clients.

Suggested approach
  } catch (error: any) {
    console.error("[Template Upload] Error:", error);
+   // Determine if error message is safe to expose
+   const safeErrors = [
+     "Template name is required",
+     "GitHub URL is required",
+     "Invalid GitHub URL format",
+     "template.config.json not found",
+     "Template with slug",
+   ];
+   const isSafeError = safeErrors.some(msg => error.message?.startsWith(msg));
    return NextResponse.json(
      {
        error: "Failed to upload template",
-       details: error.message || "Unknown error",
+       details: isSafeError ? error.message : "An unexpected error occurred",
      },
      { status: 500 },
    );
  }

Alternatively, use custom error classes to distinguish user-facing errors from internal errors.

nextjs_space/app/api/super-admin/templates/upload/route.ts-80-91 (1)

80-91: GitHub repository names can contain dots, which the regex currently rejects.

GitHub repositories allow periods (.) in their names (e.g., user/repo.name). The current pattern [\w-]+ only matches alphanumeric, underscore, and hyphen characters, causing valid repository names with dots to fail validation.

Suggested fix to support dots in repository names
-   const githubPattern = /^https:\/\/github\.com\/([\w-]+)\/([\w-]+)(\.git)?$/;
+   const githubPattern = /^https:\/\/github\.com\/([\w-]+)\/([\w.-]+)(\.git)?$/;

Comment thread nextjs_space/app/api/consultation/submit/route.ts
Comment thread nextjs_space/app/api/reset-password/route.ts
Comment thread nextjs_space/app/api/reset-password/route.ts
Comment thread nextjs_space/app/api/super-admin/platform-settings/route.ts
Comment thread nextjs_space/app/api/super-admin/tenants/route.ts
Comment thread nextjs_space/app/api/tenant-admin/webhooks/[id]/route.ts
Comment on lines 4 to 47
export async function GET(
request: NextRequest,
{ params }: { params: { slug: string } }
request: NextRequest,
{ params }: { params: { slug: string } },
) {
try {
const { slug } = params;
try {
const { slug } = params;

if (!slug) {
return NextResponse.json(
{ error: 'Slug is required' },
{ status: 400 }
);
}

// Find tenant by subdomain
const tenant = await prisma.tenants.findUnique({
where: {
subdomain: slug
},
include: {
template: true,
tenant_branding: true
}
});
if (!slug) {
return NextResponse.json({ error: "Slug is required" }, { status: 400 });
}

if (!tenant) {
return NextResponse.json(
{ error: 'Tenant not found' },
{ status: 404 }
);
}
// Find tenant by subdomain
const tenant = await prisma.tenants.findUnique({
where: {
subdomain: slug,
},
include: {
template: true,
tenant_branding: true,
},
});

return NextResponse.json({
tenant: {
id: tenant.id,
businessName: tenant.businessName,
subdomain: tenant.subdomain,
settings: tenant.settings,
template: tenant.template,
branding: tenant.tenant_branding
}
});
} catch (error) {
console.error('Error fetching tenant:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
if (!tenant) {
return NextResponse.json({ error: "Tenant not found" }, { status: 404 });
}

return NextResponse.json({
tenant: {
id: tenant.id,
businessName: tenant.businessName,
subdomain: tenant.subdomain,
settings: tenant.settings,
template: tenant.template,
branding: tenant.tenant_branding,
},
});
} catch (error) {
console.error("Error fetching tenant:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
Copy link
Copy Markdown

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:

#!/bin/bash
# Find and examine the Prisma schema
find . -name "*.prisma" -o -name "schema.prisma" | head -5

Repository: AutomatosAI/budstack-saas

Length of output: 104


🏁 Script executed:

# Search for tenant model definition in schema files
rg "model\s+tenants" --type prisma -A 30

Repository: AutomatosAI/budstack-saas

Length of output: 99


🏁 Script executed:

# Check if there are other tenant-related routes and their authentication patterns
find . -path "**/tenant/**" -name "*.ts" -o -path "**/tenant/**" -name "*.js" | grep -E "(route\.|api)" | head -20

Repository: AutomatosAI/budstack-saas

Length of output: 320


🏁 Script executed:

# Check for authentication usage in tenant routes
rg -n "getServerSession|getSession|auth|middleware" --type ts -g '**/tenant/**'

Repository: AutomatosAI/budstack-saas

Length of output: 608


🏁 Script executed:

# Look for patterns in settings field usage across the codebase
rg -n "\.settings\b" --type ts -C 2 | head -100

Repository: AutomatosAI/budstack-saas

Length of output: 7578


🏁 Script executed:

cat -n ./nextjs_space/prisma/schema.prisma | head -200

Repository: AutomatosAI/budstack-saas

Length of output: 7614


🏁 Script executed:

# Check other tenant routes for authentication
rg -n "getServerSession|getSession|auth" --type ts ./nextjs_space/app/api/tenant/current/route.ts ./nextjs_space/app/api/tenant/conditions/route.ts -A 5

Repository: AutomatosAI/budstack-saas

Length of output: 51


🏁 Script executed:

# Look at the full tenant/current route to understand its purpose
cat -n ./nextjs_space/app/api/tenant/current/route.ts

Repository: AutomatosAI/budstack-saas

Length of output: 711


🏁 Script executed:

# Find and show the tenants model in the Prisma schema
rg "^model tenants" --type ts -A 40 ./nextjs_space/prisma/schema.prisma

Repository: AutomatosAI/budstack-saas

Length of output: 3236


🏁 Script executed:

# Check the getCurrentTenant implementation
find . -name "*.ts" -path "**/lib/**" | xargs grep -l "getCurrentTenant" | head -3

Repository: AutomatosAI/budstack-saas

Length of output: 132


🏁 Script executed:

# Read the getCurrentTenant function
fd -e ts -path "**/lib/**" | xargs grep -l "export.*getCurrentTenant" | head -1 | xargs cat

Repository: AutomatosAI/budstack-saas

Length of output: 237


🏁 Script executed:

# Read the tenant.ts file to find getCurrentTenant
cat -n ./nextjs_space/lib/tenant.ts

Repository: AutomatosAI/budstack-saas

Length of output: 5842


🏁 Script executed:

# Also check tenant-utils.ts
cat -n ./nextjs_space/lib/tenant-utils.ts

Repository: AutomatosAI/budstack-saas

Length of output: 1325


Filter sensitive fields from the public tenant endpoint.

This route exposes the full settings field publicly, which contains SMTP credentials (host, user, password). These secrets should never be returned to unauthenticated clients. Either require authentication for this endpoint, or filter sensitive data before returning—only expose public tenant information like businessName, subdomain, template, and branding.

Comment thread nextjs_space/app/api/webhooks/drgreen/crypto/route.ts
Comment on lines +96 to +127
// Trigger BudStack webhook
if (paymentStatus === "PAID") {
await triggerWebhook({
event: WEBHOOK_EVENTS.ORDER_CONFIRMED,
tenantId: order.tenantId,
data: {
orderId: order.id,
orderNumber: order.orderNumber,
drGreenOrderId: order.drGreenOrderId,
paymentMethod: "FIAT",
amount: parseFloat(amount || "0"),
currency: currency || "USD",
invoiceId: payment_id,
customerEmail: order.user.email,
},
});

console.log("[Fiat Webhook] Order paid successfully:", order.id);
} else if (paymentStatus === "FAILED") {
await triggerWebhook({
event: WEBHOOK_EVENTS.ORDER_CANCELLED,
tenantId: order.tenantId,
data: {
orderId: order.id,
orderNumber: order.orderNumber,
reason: "Payment failed",
customerEmail: order.user.email,
},
});

console.log("[Fiat Webhook] Payment failed for order:", order.id);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Potential null reference on order.user.email.

If an order exists without an associated user (e.g., guest checkout or data inconsistency), accessing order.user.email at lines 109 and 122 will throw a runtime error, causing the webhook to fail after the order has already been updated.

🐛 Add null safety
           invoiceId: payment_id,
-          customerEmail: order.user.email,
+          customerEmail: order.user?.email ?? null,
         },
       });

Apply the same fix at line 122.

Additionally, parseFloat(amount || "0") at line 106 can return NaN for malformed strings—consider validating or defaulting:

-          amount: parseFloat(amount || "0"),
+          amount: Number.isFinite(parseFloat(amount)) ? parseFloat(amount) : 0,
🤖 Prompt for AI Agents
In @nextjs_space/app/api/webhooks/drgreen/fiat/route.ts around lines 96 - 127,
The webhook handler uses order.user.email and parseFloat(amount || "0")
unsafely; update the triggerWebhook calls (the blocks invoking triggerWebhook
for WEBHOOK_EVENTS.ORDER_CONFIRMED and ORDER_CANCELLED) to read the customer
email via a null-safe expression (e.g., get a local customerEmail =
order.user?.email ?? "" or a clearly named fallback) and pass that instead of
order.user.email, and validate/normalize amount before sending (e.g.,
parseFloat(amount) with an isNaN check or Number() and fallback to 0) so the
data object always contains a safe string for customerEmail and a finite numeric
amount for the payment payload.

Comment thread nextjs_space/app/api/webhooks/drgreen/fiat/route.ts
@AutomatosAI AutomatosAI merged commit bcd63ba into main Jan 13, 2026
1 check was pending
@AutomatosAI AutomatosAI deleted the review/phase3-api branch January 13, 2026 16:33
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